Initial Release
This commit is contained in:
parent
123855d477
commit
1c2604bea0
17
.github/CODEOWNERS
vendored
Normal file
17
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#
|
||||||
|
# CODEOWNERS for ProxmoxVE
|
||||||
|
#
|
||||||
|
|
||||||
|
# Order is important; the last matching pattern takes the most
|
||||||
|
# precedence.
|
||||||
|
|
||||||
|
|
||||||
|
# Codeowners for specific folders and files
|
||||||
|
# Remember ending folders with /
|
||||||
|
|
||||||
|
|
||||||
|
# Set default reviewers
|
||||||
|
* @community-scripts/Contributor
|
||||||
|
|
||||||
|
# All changes in frontend
|
||||||
|
/frontend/ @community-scripts/Frontend-Dev
|
14
.github/CONTRIBUTOR_AND_GUIDES/CODE-AUDIT.md
vendored
Normal file
14
.github/CONTRIBUTOR_AND_GUIDES/CODE-AUDIT.md
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo.png" height="100px" />
|
||||||
|
</div>
|
||||||
|
<h2><div align="center">Exploring the Scripts and Steps Involved in an Application LXC Installation</div></h2>
|
||||||
|
|
||||||
|
1) [adguard.sh](https://github.com/community-scripts/ProxmoxVE/blob/main/ct/adguard.sh): This script collects system parameters. (Also holds the function to update the application.)
|
||||||
|
2) [build.func](https://github.com/community-scripts/ProxmoxVE/blob/main/misc/build.func): Adds user settings and integrates collected information.
|
||||||
|
3) [create_lxc.sh](https://github.com/community-scripts/ProxmoxVE/blob/main/ct/create_lxc.sh): Constructs the LXC container.
|
||||||
|
4) [adguard-install.sh](https://github.com/community-scripts/ProxmoxVE/blob/main/install/adguard-install.sh): Executes functions from [install.func](https://github.com/community-scripts/ProxmoxVE/blob/main/misc/install.func), and installs the application.
|
||||||
|
5) [adguard.sh](https://github.com/community-scripts/ProxmoxVE/blob/main/ct/adguard.sh) (again): To display the completion message.
|
||||||
|
|
||||||
|
The installation process uses reusable scripts: [build.func](https://github.com/community-scripts/ProxmoxVE/blob/main/misc/build.func), [create_lxc.sh](https://github.com/community-scripts/ProxmoxVE/blob/main/ct/create_lxc.sh), and [install.func](https://github.com/community-scripts/ProxmoxVE/blob/main/misc/install.func), which are not specific to any particular application.
|
||||||
|
|
||||||
|
To gain a better understanding, focus on reviewing [adguard-install.sh](https://github.com/community-scripts/ProxmoxVE/blob/main/install/adguard-install.sh). This script contains the commands and configurations for installing and configuring AdGuard Home within the LXC container.
|
132
.github/CONTRIBUTOR_AND_GUIDES/CODE_OF_CONDUCT.md
vendored
Normal file
132
.github/CONTRIBUTOR_AND_GUIDES/CODE_OF_CONDUCT.md
vendored
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||||
|
at [https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
106
.github/CONTRIBUTOR_AND_GUIDES/CONTRIBUTING.md
vendored
Normal file
106
.github/CONTRIBUTOR_AND_GUIDES/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
|
||||||
|
# Community Scripts Contribution Guide
|
||||||
|
|
||||||
|
## **Welcome to the communty-scripts Repository!**
|
||||||
|
|
||||||
|
📜 These documents outline the essential coding standards for all our scripts and JSON files. Adhering to these standards ensures that our codebase remains consistent, readable, and maintainable. By following these guidelines, we can improve collaboration, reduce errors, and enhance the overall quality of our project.
|
||||||
|
|
||||||
|
### Why Coding Standards Matter
|
||||||
|
|
||||||
|
Coding standards are crucial for several reasons:
|
||||||
|
|
||||||
|
1. **Consistency**: Consistent code is easier to read, understand, and maintain. It helps new team members quickly get up to speed and reduces the learning curve.
|
||||||
|
2. **Readability**: Clear and well-structured code is easier to debug and extend. It allows developers to quickly identify and fix issues.
|
||||||
|
3. **Maintainability**: Code that follows a standard structure is easier to refactor and update. It ensures that changes can be made with minimal risk of introducing new bugs.
|
||||||
|
4. **Collaboration**: When everyone follows the same standards, it becomes easier to collaborate on code. It reduces friction and misunderstandings during code reviews and merges.
|
||||||
|
|
||||||
|
### Scope of These Documents
|
||||||
|
|
||||||
|
These documents cover the coding standards for the following types of files in our project:
|
||||||
|
|
||||||
|
- **`install/$AppName-install.sh` Scripts**: These scripts are responsible for the installation of applications.
|
||||||
|
- **`ct/$AppName.sh` Scripts**: These scripts handle the creation and updating of containers.
|
||||||
|
- **`json/$AppName.json`**: These files store structured data and are used for the website.
|
||||||
|
|
||||||
|
Each section provides detailed guidelines on various aspects of coding, including shebang usage, comments, variable naming, function naming, indentation, error handling, command substitution, quoting, script structure, and logging. Additionally, examples are provided to illustrate the application of these standards.
|
||||||
|
|
||||||
|
By following the coding standards outlined in this document, we ensure that our scripts and JSON files are of high quality, making our project more robust and easier to manage. Please refer to this guide whenever you create or update scripts and JSON files to maintain a high standard of code quality across the project. 📚🔍
|
||||||
|
|
||||||
|
Let's work together to keep our codebase clean, efficient, and maintainable! 💪🚀
|
||||||
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Before contributing, please ensure that you have the following setup:
|
||||||
|
|
||||||
|
1. **Visual Studio Code** (recommended for script development)
|
||||||
|
2. **Recommended VS Code Extensions:**
|
||||||
|
- [Shell Syntax](https://marketplace.visualstudio.com/items?itemName=bmalehorn.shell-syntax)
|
||||||
|
- [ShellCheck](https://marketplace.visualstudio.com/items?itemName=timonwong.shellcheck)
|
||||||
|
- [Shell Format](https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format)
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
- Use [AppName.sh](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/ct/AppName.sh) and [AppName-install.sh](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.sh) as templates when creating new scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🚀 The Application Script (ct/AppName.sh)
|
||||||
|
|
||||||
|
- You can find all coding standards, as well as the structure for this file [here](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/ct/AppName.md).
|
||||||
|
- These scripts are responsible for container creation, setting the necessary variables and handling the update of the application once installed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🛠 The Installation Script (install/AppName-install.sh)
|
||||||
|
|
||||||
|
- You can find all coding standards, as well as the structure for this file [here](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.md).
|
||||||
|
- These scripts are responsible for the installation of the application.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Building Your Own Scripts
|
||||||
|
|
||||||
|
Start with the [template script](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.sh)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contribution Process
|
||||||
|
|
||||||
|
### 1. Fork the repository
|
||||||
|
Fork to your GitHub account
|
||||||
|
|
||||||
|
### 2. Clone your fork on your local environment
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourUserName/ForkName
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create a new branch
|
||||||
|
```bash
|
||||||
|
git switch -c your-feature-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Change paths in build.func install.func and AppName.sh
|
||||||
|
To be able to develop from your own branch you need to change `https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main` to `https://raw.githubusercontent.com/[USER]/[REPOSITORY]/refs/heads/[BRANCH]`. You need to make this change atleast in misc/build.func misc/install.func and in your ct/AppName.sh. This change is only for testing. Before opening a Pull Request you should change this line change all this back to point to `https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main`.
|
||||||
|
|
||||||
|
### 4. Commit changes (without build.func and install.func!)
|
||||||
|
```bash
|
||||||
|
git commit -m "Your commit message"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Push to your fork
|
||||||
|
```bash
|
||||||
|
git push origin your-feature-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Create a Pull Request
|
||||||
|
Open a Pull Request from your feature branch to the main repository branch. You must only include your **$AppName.sh**, **$AppName-install.sh** and **$AppName.json** files in the pull request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Pages
|
||||||
|
|
||||||
|
- [CT Template: AppName.sh](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/ct/AppName.sh)
|
||||||
|
- [Install Template: AppName-install.sh](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.sh)
|
||||||
|
- [JSON Template: AppName.json](https://github.com/community-scripts/ProxmoxVED/blob/main/.github/CONTRIBUTOR_AND_GUIDES/json/AppName.json)
|
||||||
|
|
||||||
|
|
44
.github/CONTRIBUTOR_AND_GUIDES/USER_SUBMITTED_GUIDES.md
vendored
Normal file
44
.github/CONTRIBUTOR_AND_GUIDES/USER_SUBMITTED_GUIDES.md
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="#">
|
||||||
|
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/images/logo.png" height="100px" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h2 align="center">User Submitted Guides </h2>
|
||||||
|
|
||||||
|
<sub> In order to contribute a guide on installing with Proxmox VE Helper Scripts, you should open a pull request that adds the guide to the `USER_SUBMITTED_GUIDES.md` file. </sub>
|
||||||
|
|
||||||
|
[Proxmox Automation with Proxmox Helper Scripts!](https://www.youtube.com/watch?v=kcpu4z5eSEU)
|
||||||
|
|
||||||
|
[Installing Home Assistant OS using Proxmox 8](https://community.home-assistant.io/t/installing-home-assistant-os-using-proxmox-8/201835)
|
||||||
|
|
||||||
|
[How To Separate Zigbee2MQTT From Home Assistant In Proxmox](https://smarthomescene.com/guides/how-to-separate-zigbee2mqtt-from-home-assistant-in-proxmox/)
|
||||||
|
|
||||||
|
[How To Install Home Assistant On Proxmox: The Easy Way](https://smarthomescene.com/guides/how-to-install-home-assistant-on-proxmox-the-easy-way/)
|
||||||
|
|
||||||
|
[Home Assistant: Installing InfluxDB (LXC)](https://www.derekseaman.com/2023/04/home-assistant-installing-influxdb-lxc.html)
|
||||||
|
|
||||||
|
[Home Assistant: Proxmox Quick Start Guide](https://www.derekseaman.com/2023/10/home-assistant-proxmox-ve-8-0-quick-start-guide-2.html)
|
||||||
|
|
||||||
|
[Home Assistant: Installing Grafana (LXC) with Let’s Encrypt SSL](https://www.derekseaman.com/2023/04/home-assistant-installing-grafana-lxc.html)
|
||||||
|
|
||||||
|
[Proxmox: Plex LXC with Alder Lake Transcoding](https://www.derekseaman.com/2023/04/proxmox-plex-lxc-with-alder-lake-transcoding.html)
|
||||||
|
|
||||||
|
[How To Backup Home Assistant In Proxmox](https://smarthomescene.com/guides/how-to-backup-home-assistant-in-proxmox/)
|
||||||
|
|
||||||
|
[Running Frigate on Proxmox](https://www.homeautomationguy.io/blog/running-frigate-on-proxmox)
|
||||||
|
|
||||||
|
[Frigate VM on Proxmox with PCIe Coral TPU](https://www.derekseaman.com/2023/06/home-assistant-frigate-vm-on-proxmox-with-pcie-coral-tpu.html)
|
||||||
|
|
||||||
|
[Moving Home Assistant’s Database To MariaDB On Proxmox](https://smarthomescene.com/guides/moving-home-assistants-database-to-mariadb-on-proxmox/)
|
||||||
|
|
||||||
|
[How-to: Proxmox VE 7.4 to 8.0 Upgrade](https://www.derekseaman.com/2023/06/how-to-proxmox-7-4-to-8-0-upgrade.html)
|
||||||
|
|
||||||
|
[iGPU Transcoding In Proxmox with Jellyfin](https://www.youtube.com/watch?v=XAa_qpNmzZs)
|
||||||
|
|
||||||
|
[Proxmox + NetData](<https://dbt3ch.com/books/proxmox-netdata-for-better-insights-and-notifications/page/proxmox-netdata-for-better-insights-and-notifications>)
|
||||||
|
|
||||||
|
[Proxmox Homelab Series](<https://blog.kye.dev/proxmox-series>)
|
||||||
|
|
||||||
|
[The fastest installation of Docker and Portainer on Proxmox VE](https://lavr.site/en-fastest-install-docker-portainer-proxmox/)
|
||||||
|
|
||||||
|
[How To Setup Proxmox Backuper Server Using Helper Scripts](<https://youtu.be/6C2JOsrZZZw?si=kkrrcL_nLCDBJkOB>)
|
286
.github/CONTRIBUTOR_AND_GUIDES/ct/AppName.md
vendored
Normal file
286
.github/CONTRIBUTOR_AND_GUIDES/ct/AppName.md
vendored
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
# **AppName<span></span>.sh Scripts**
|
||||||
|
|
||||||
|
`AppName.sh` scripts found in the `/ct` directory. These scripts are responsible for the installation of the desired application. For this guide we take `/ct/snipeit.sh` as example.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [**AppName.sh Scripts**](#appnamesh-scripts)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [1. **File Header**](#1-file-header)
|
||||||
|
- [1.1 **Shebang**](#11-shebang)
|
||||||
|
- [1.2 **Import Functions**](#12-import-functions)
|
||||||
|
- [1.3 **Metadata**](#13-metadata)
|
||||||
|
- [2 **Variables and function import**](#2-variables-and-function-import)
|
||||||
|
- [2.1 **Default Values**](#21-default-values)
|
||||||
|
- [2.2 **📋 App output \& base settings**](#22--app-output--base-settings)
|
||||||
|
- [2.3 **🛠 Core functions**](#23--core-functions)
|
||||||
|
- [3 **Update function**](#3-update-function)
|
||||||
|
- [3.1 **Function Header**](#31-function-header)
|
||||||
|
- [3.2 **Check APP**](#32-check-app)
|
||||||
|
- [3.3 **Check version**](#33-check-version)
|
||||||
|
- [3.4 **Verbosity**](#34-verbosity)
|
||||||
|
- [3.5 **Backups**](#35-backups)
|
||||||
|
- [3.6 **Cleanup**](#36-cleanup)
|
||||||
|
- [3.7 **No update function**](#37-no-update-function)
|
||||||
|
- [4 **End of the script**](#4-end-of-the-script)
|
||||||
|
- [5. **Contribution checklist**](#5-contribution-checklist)
|
||||||
|
|
||||||
|
## 1. **File Header**
|
||||||
|
|
||||||
|
### 1.1 **Shebang**
|
||||||
|
|
||||||
|
- Use `#!/usr/bin/env bash` as the shebang.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 **Import Functions**
|
||||||
|
|
||||||
|
- Import the build.func file.
|
||||||
|
- When developing your own script, change the URL to your own repository.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> You also need to change all apperances of this URL in `misc/build.func` and `misc/install.func`
|
||||||
|
|
||||||
|
Example for development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source <(curl -s https://raw.githubusercontent.com/[USER]/[REPO]/refs/heads/[BRANCH]/misc/build.func)
|
||||||
|
```
|
||||||
|
|
||||||
|
Final script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> Before opening a Pull Request, change the URLs to point to the community-scripts repo.
|
||||||
|
|
||||||
|
### 1.3 **Metadata**
|
||||||
|
|
||||||
|
- Add clear comments for script metadata, including author, copyright, and license information.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: [YourUserName]
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
# Source: [SOURCE_URL]
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]:
|
||||||
|
>
|
||||||
|
> - Add your username and source URL
|
||||||
|
> - For existing scripts, add "| Co-Author [YourUserName]" after the current author
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2 **Variables and function import**
|
||||||
|
>
|
||||||
|
> [!NOTE]
|
||||||
|
> You need to have all this set in your script, otherwise it will not work!
|
||||||
|
|
||||||
|
### 2.1 **Default Values**
|
||||||
|
|
||||||
|
- This section sets the default values for the container.
|
||||||
|
- `APP` needs to be set to the application name and must be equal to the filenames of your scripts.
|
||||||
|
- `var_tags`: You can set Tags for the CT wich show up in the Proxmox UI. Don´t overdo it!
|
||||||
|
|
||||||
|
>[!NOTE]
|
||||||
|
>Description for all Default Values
|
||||||
|
>
|
||||||
|
>| Variable | Description | Notes |
|
||||||
|
>|----------|-------------|-------|
|
||||||
|
>| `APP` | Application name | Must match ct\AppName.sh |
|
||||||
|
>| `var_tags` | Proxmox display tags without Spaces, only ; | Limit the number |
|
||||||
|
>| `var_cpu` | CPU cores | Number of cores |
|
||||||
|
>| `var_ram` | RAM | In MB |
|
||||||
|
>| `var_disk` | Disk capacity | In GB |
|
||||||
|
>| `var_os` | Operating system | alpine, debian, ubuntu |
|
||||||
|
>| `var_version` | OS version | e.g., 3.20, 11, 12, 20.04 |
|
||||||
|
>| `var_unprivileged` | Container type | 1 = Unprivileged, 0 = Privileged |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP="SnipeIT"
|
||||||
|
var_tags="asset-management;foss"
|
||||||
|
var_cpu="2"
|
||||||
|
var_ram="2048"
|
||||||
|
var_disk="4"
|
||||||
|
var_os="debian"
|
||||||
|
var_version="12"
|
||||||
|
var_unprivileged="1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.2 **📋 App output & base settings**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
header_info "$APP"
|
||||||
|
```
|
||||||
|
- `header_info`: Generates ASCII header for APP
|
||||||
|
|
||||||
|
## 2.3 **🛠 Core functions**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
```
|
||||||
|
|
||||||
|
- `variables`: Processes input and prepares variables
|
||||||
|
- `color`: Sets icons, colors, and formatting
|
||||||
|
- `catch_errors`: Enables error handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3 **Update function**
|
||||||
|
|
||||||
|
### 3.1 **Function Header**
|
||||||
|
|
||||||
|
- If applicable write a function that updates the application and the OS in the container.
|
||||||
|
- Each update function starts with the same code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 **Check APP**
|
||||||
|
|
||||||
|
- Before doing anything update-wise, check if the app is installed in the container.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ ! -d /opt/snipe-it ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 **Check version**
|
||||||
|
|
||||||
|
- Before updating, check if a new version exists.
|
||||||
|
- We use the `${APPLICATION}_version.txt` file created in `/opt` during the install to compare new versions against the currently installed version.
|
||||||
|
|
||||||
|
Example with a Github Release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RELEASE=$(curl -fsSL https://api.github.com/repos/snipe/snipe-it/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
|
||||||
|
if [[ ! -f /opt/${APP}_version.txt ]] || [[ "${RELEASE}" != "$(cat /opt/${APP}_version.txt)" ]]; then
|
||||||
|
msg_info "Updating ${APP} to v${RELEASE}"
|
||||||
|
#DO UPDATE
|
||||||
|
else
|
||||||
|
msg_ok "No update required. ${APP} is already at v${RELEASE}."
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 **Verbosity**
|
||||||
|
|
||||||
|
- Use the appropriate flag (**-q** in the examples) for a command to suppress its output.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget -q
|
||||||
|
unzip -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- If a command does not come with this functionality use `$STD` to suppress it's output.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$STD php artisan migrate --force
|
||||||
|
$STD php artisan config:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 **Backups**
|
||||||
|
|
||||||
|
- Backup user data if necessary.
|
||||||
|
- Move all user data back in the directory when the update is finished.
|
||||||
|
|
||||||
|
>[!NOTE]
|
||||||
|
>This is not meant to be a permanent backup
|
||||||
|
|
||||||
|
Example backup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv /opt/snipe-it /opt/snipe-it-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
Example config restore:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /opt/snipe-it-backup/.env /opt/snipe-it/.env
|
||||||
|
cp -r /opt/snipe-it-backup/public/uploads/ /opt/snipe-it/public/uploads/
|
||||||
|
cp -r /opt/snipe-it-backup/storage/private_uploads /opt/snipe-it/storage/private_uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 **Cleanup**
|
||||||
|
|
||||||
|
- Do not forget to remove any temporary files/folders such as zip-files or temporary backups.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /opt/v${RELEASE}.zip
|
||||||
|
rm -rf /opt/snipe-it-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 **No update function**
|
||||||
|
|
||||||
|
- In case you can not provide an update function use the following code to provide user feedback.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
if [[ ! -d /opt/snipeit ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
msg_error "Currently we don't provide an update function for this ${APP}."
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4 **End of the script**
|
||||||
|
|
||||||
|
- `start`: Launches Whiptail dialogue
|
||||||
|
- `build_container`: Collects and integrates user settings
|
||||||
|
- `description`: Sets LXC container description
|
||||||
|
- With `echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"` you can point the user to the IP:PORT/folder needed to access the app.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. **Contribution checklist**
|
||||||
|
|
||||||
|
- [ ] Shebang is correctly set (`#!/usr/bin/env bash`).
|
||||||
|
- [ ] Correct link to *build.func*
|
||||||
|
- [ ] Metadata (author, license) is included at the top.
|
||||||
|
- [ ] Variables follow naming conventions.
|
||||||
|
- [ ] Update function exists.
|
||||||
|
- [ ] Update functions checks if app is installed and for new version.
|
||||||
|
- [ ] Update function cleans up temporary files.
|
||||||
|
- [ ] Script ends with a helpful message for the user to reach the application.
|
86
.github/CONTRIBUTOR_AND_GUIDES/ct/AppName.sh
vendored
Normal file
86
.github/CONTRIBUTOR_AND_GUIDES/ct/AppName.sh
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: [YourUserName]
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
# Source: [SOURCE_URL]
|
||||||
|
|
||||||
|
# App Default Values
|
||||||
|
APP="[APP_NAME]"
|
||||||
|
# Name of the app (e.g. Google, Adventurelog, Apache-Guacamole"
|
||||||
|
var_tags="[TAGS]"
|
||||||
|
# Tags for Proxmox VE, maximum 2 pcs., no spaces allowed, separated by a semicolon ; (e.g. database | adblock;dhcp)
|
||||||
|
var_cpu="[CPU]"
|
||||||
|
# Number of cores (1-X) (e.g. 4) - default are 2
|
||||||
|
var_ram="[RAM]"
|
||||||
|
# Amount of used RAM in MB (e.g. 2048 or 4096)
|
||||||
|
var_disk="[DISK]"
|
||||||
|
# Amount of used disk space in GB (e.g. 4 or 10)
|
||||||
|
var_os="[OS]"
|
||||||
|
# Default OS (e.g. debian, ubuntu, alpine)
|
||||||
|
var_version="[VERSION]"
|
||||||
|
# Default OS version (e.g. 12 for debian, 24.04 for ubuntu, 3.20 for alpine)
|
||||||
|
var_unprivileged="[UNPRIVILEGED]"
|
||||||
|
# 1 = unprivileged container, 0 = privileged container
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
|
||||||
|
# Check if installation is present | -f for file, -d for folder
|
||||||
|
if [[ ! -f [INSTALLATION_CHECK_PATH] ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Crawling the new version and checking whether an update is required
|
||||||
|
RELEASE=$(curl -fsSL [RELEASE_URL] | [PARSE_RELEASE_COMMAND])
|
||||||
|
if [[ "${RELEASE}" != "$(cat /opt/${APP}_version.txt)" ]] || [[ ! -f /opt/${APP}_version.txt ]]; then
|
||||||
|
# Stopping Services
|
||||||
|
msg_info "Stopping $APP"
|
||||||
|
systemctl stop [SERVICE_NAME]
|
||||||
|
msg_ok "Stopped $APP"
|
||||||
|
|
||||||
|
# Creating Backup
|
||||||
|
msg_info "Creating Backup"
|
||||||
|
tar -czf "/opt/${APP}_backup_$(date +%F).tar.gz" [IMPORTANT_PATHS]
|
||||||
|
msg_ok "Backup Created"
|
||||||
|
|
||||||
|
# Execute Update
|
||||||
|
msg_info "Updating $APP to v${RELEASE}"
|
||||||
|
[UPDATE_COMMANDS]
|
||||||
|
msg_ok "Updated $APP to v${RELEASE}"
|
||||||
|
|
||||||
|
# Starting Services
|
||||||
|
msg_info "Starting $APP"
|
||||||
|
systemctl start [SERVICE_NAME]
|
||||||
|
msg_ok "Started $APP"
|
||||||
|
|
||||||
|
# Cleaning up
|
||||||
|
msg_info "Cleaning Up"
|
||||||
|
rm -rf [TEMP_FILES]
|
||||||
|
msg_ok "Cleanup Completed"
|
||||||
|
|
||||||
|
# Last Action
|
||||||
|
echo "${RELEASE}" >/opt/${APP}_version.txt
|
||||||
|
msg_ok "Update Successful"
|
||||||
|
else
|
||||||
|
msg_ok "No update required. ${APP} is already at v${RELEASE}"
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:[PORT]${CL}"
|
353
.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.md
vendored
Normal file
353
.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.md
vendored
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
|
||||||
|
# **AppName<span></span>-install.sh Scripts**
|
||||||
|
|
||||||
|
`AppName-install.sh` scripts found in the `/install` directory. These scripts are responsible for the installation of the application. For this guide we take `/install/snipeit-install.sh` as example.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [**AppName-install.sh Scripts**](#appname-installsh-scripts)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [1. **File header**](#1-file-header)
|
||||||
|
- [1.1 **Shebang**](#11-shebang)
|
||||||
|
- [1.2 **Comments**](#12-comments)
|
||||||
|
- [1.3 **Variables and function import**](#13-variables-and-function-import)
|
||||||
|
- [2. **Variable naming and management**](#2-variable-naming-and-management)
|
||||||
|
- [2.1 **Naming conventions**](#21-naming-conventions)
|
||||||
|
- [3. **Dependencies**](#3-dependencies)
|
||||||
|
- [3.1 **Install all at once**](#31-install-all-at-once)
|
||||||
|
- [3.2 **Collapse dependencies**](#32-collapse-dependencies)
|
||||||
|
- [4. **Paths to application files**](#4-paths-to-application-files)
|
||||||
|
- [5. **Version management**](#5-version-management)
|
||||||
|
- [5.1 **Install the latest release**](#51-install-the-latest-release)
|
||||||
|
- [5.2 **Save the version for update checks**](#52-save-the-version-for-update-checks)
|
||||||
|
- [6. **Input and output management**](#6-input-and-output-management)
|
||||||
|
- [6.1 **User feedback**](#61-user-feedback)
|
||||||
|
- [6.2 **Verbosity**](#62-verbosity)
|
||||||
|
- [7. **String/File Manipulation**](#7-stringfile-manipulation)
|
||||||
|
- [7.1 **File Manipulation**](#71-file-manipulation)
|
||||||
|
- [8. **Security practices**](#8-security-practices)
|
||||||
|
- [8.1 **Password generation**](#81-password-generation)
|
||||||
|
- [8.2 **File permissions**](#82-file-permissions)
|
||||||
|
- [9. **Service Configuration**](#9-service-configuration)
|
||||||
|
- [9.1 **Configuration files**](#91-configuration-files)
|
||||||
|
- [9.2 **Credential management**](#92-credential-management)
|
||||||
|
- [9.3 **Enviroment files**](#93-enviroment-files)
|
||||||
|
- [9.4 **Services**](#94-services)
|
||||||
|
- [10. **Cleanup**](#10-cleanup)
|
||||||
|
- [10.1 **Remove temporary files**](#101-remove-temporary-files)
|
||||||
|
- [10.2 **Autoremove and autoclean**](#102-autoremove-and-autoclean)
|
||||||
|
- [11. **Best Practices Checklist**](#11-best-practices-checklist)
|
||||||
|
- [Example: High-Level Script Flow](#example-high-level-script-flow)
|
||||||
|
|
||||||
|
## 1. **File header**
|
||||||
|
|
||||||
|
### 1.1 **Shebang**
|
||||||
|
|
||||||
|
- Use `#!/usr/bin/env bash` as the shebang.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 **Comments**
|
||||||
|
|
||||||
|
- Add clear comments for script metadata, including author, copyright, and license information.
|
||||||
|
- Use meaningful inline comments to explain complex commands or logic.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: [YourUserName]
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
# Source: [SOURCE_URL]
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]:
|
||||||
|
>
|
||||||
|
> - Add your username
|
||||||
|
> - When updating/reworking scripts, add "| Co-Author [YourUserName]"
|
||||||
|
|
||||||
|
### 1.3 **Variables and function import**
|
||||||
|
|
||||||
|
- This sections adds the support for all needed functions and variables.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. **Variable naming and management**
|
||||||
|
|
||||||
|
### 2.1 **Naming conventions**
|
||||||
|
|
||||||
|
- Use uppercase names for constants and environment variables.
|
||||||
|
- Use lowercase names for local script variables.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DB_NAME=snipeit_db # Environment-like variable (constant)
|
||||||
|
db_user="snipeit" # Local variable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. **Dependencies**
|
||||||
|
|
||||||
|
### 3.1 **Install all at once**
|
||||||
|
|
||||||
|
- Install all dependencies with a single command if possible
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$STD apt-get install -y \
|
||||||
|
curl \
|
||||||
|
composer \
|
||||||
|
git \
|
||||||
|
sudo \
|
||||||
|
mc \
|
||||||
|
nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 **Collapse dependencies**
|
||||||
|
|
||||||
|
Collapse dependencies to keep the code readable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php8.2-{bcmath,common,ctype}
|
||||||
|
```
|
||||||
|
|
||||||
|
instead of
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php8.2-bcmath php8.2-common php8.2-ctype
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. **Paths to application files**
|
||||||
|
|
||||||
|
If possible install the app and all necessary files in `/opt/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. **Version management**
|
||||||
|
|
||||||
|
### 5.1 **Install the latest release**
|
||||||
|
|
||||||
|
- Always try and install the latest release
|
||||||
|
- Do not hardcode any version if not absolutely necessary
|
||||||
|
|
||||||
|
Example for a git release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RELEASE=$(curl -fsSL https://api.github.com/repos/snipe/snipe-it/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
|
||||||
|
wget -q "https://github.com/snipe/snipe-it/archive/refs/tags/v${RELEASE}.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 **Save the version for update checks**
|
||||||
|
|
||||||
|
- Write the installed version into a file.
|
||||||
|
- This is used for the update function in **AppName.sh** to check for if a Update is needed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "${RELEASE}" >"/opt/AppName_version.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. **Input and output management**
|
||||||
|
|
||||||
|
### 6.1 **User feedback**
|
||||||
|
|
||||||
|
- Use standard functions like `msg_info`, `msg_ok` or `msg_error` to print status messages.
|
||||||
|
- Each `msg_info` must be followed with a `msg_ok` before any other output is made.
|
||||||
|
- Display meaningful progress messages at key stages.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
msg_info "Installing Dependencies"
|
||||||
|
$STD apt-get install -y ...
|
||||||
|
msg_ok "Installed Dependencies"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 **Verbosity**
|
||||||
|
|
||||||
|
- Use the appropiate flag (**-q** in the examples) for a command to suppres its output
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget -q
|
||||||
|
unzip -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- If a command dose not come with such a functionality use `$STD` (a custom standard redirection variable) for managing output verbosity.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$STD apt-get install -y nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. **String/File Manipulation**
|
||||||
|
|
||||||
|
### 7.1 **File Manipulation**
|
||||||
|
|
||||||
|
- Use `sed` to replace placeholder values in configuration files.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sed -i -e "s|^DB_DATABASE=.*|DB_DATABASE=$DB_NAME|" \
|
||||||
|
-e "s|^DB_USERNAME=.*|DB_USERNAME=$DB_USER|" \
|
||||||
|
-e "s|^DB_PASSWORD=.*|DB_PASSWORD=$DB_PASS|" .env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. **Security practices**
|
||||||
|
|
||||||
|
### 8.1 **Password generation**
|
||||||
|
|
||||||
|
- Use `openssl` to generate random passwords.
|
||||||
|
- Use only alphanumeric values to not introduce unknown behaviour.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 **File permissions**
|
||||||
|
|
||||||
|
Explicitly set secure ownership and permissions for sensitive files.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chown -R www-data: /opt/snipe-it
|
||||||
|
chmod -R 755 /opt/snipe-it
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. **Service Configuration**
|
||||||
|
|
||||||
|
### 9.1 **Configuration files**
|
||||||
|
|
||||||
|
Use `cat <<EOF` to write configuration files in a clean and readable way.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat <<EOF >/etc/nginx/conf.d/snipeit.conf
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /opt/snipe-it/public;
|
||||||
|
index index.php;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 **Credential management**
|
||||||
|
|
||||||
|
Store the generated credentials in a file.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
USERNAME=username
|
||||||
|
PASSWORD=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
|
||||||
|
{
|
||||||
|
echo "Application-Credentials"
|
||||||
|
echo "Username: $USERNAME"
|
||||||
|
echo "Password: $PASSWORD"
|
||||||
|
} >> ~/application.creds
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 **Enviroment files**
|
||||||
|
|
||||||
|
Use `cat <<EOF` to write enviromental files in a clean and readable way.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat <<EOF >/path/to/.env
|
||||||
|
VARIABLE="value"
|
||||||
|
PORT=3000
|
||||||
|
DB_NAME="${DB_NAME}"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.4 **Services**
|
||||||
|
|
||||||
|
Enable affected services after configuration changes and start them right away.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable -q --now nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. **Cleanup**
|
||||||
|
|
||||||
|
### 10.1 **Remove temporary files**
|
||||||
|
|
||||||
|
Remove temporary files and downloads after use.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /opt/v${RELEASE}.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 **Autoremove and autoclean**
|
||||||
|
|
||||||
|
Remove unused dependencies to reduce disk space usage.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get -y autoremove
|
||||||
|
apt-get -y autoclean
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. **Best Practices Checklist**
|
||||||
|
|
||||||
|
- [ ] Shebang is correctly set (`#!/usr/bin/env bash`).
|
||||||
|
- [ ] Metadata (author, license) is included at the top.
|
||||||
|
- [ ] Variables follow naming conventions.
|
||||||
|
- [ ] Sensitive values are dynamically generated.
|
||||||
|
- [ ] Files and services have proper permissions.
|
||||||
|
- [ ] Script cleans up temporary files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example: High-Level Script Flow
|
||||||
|
|
||||||
|
1. Dependencies installation
|
||||||
|
2. Database setup
|
||||||
|
3. Download and configure application
|
||||||
|
4. Service configuration
|
||||||
|
5. Final cleanup
|
83
.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.sh
vendored
Normal file
83
.github/CONTRIBUTOR_AND_GUIDES/install/AppName-install.sh
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: [YourUserName]
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
# Source: [SOURCE_URL]
|
||||||
|
|
||||||
|
# Import Functions und Setup
|
||||||
|
source /dev/stdin <<< "$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
# Installing Dependencies with the 3 core dependencies (curl;sudo;mc)
|
||||||
|
msg_info "Installing Dependencies"
|
||||||
|
$STD apt-get install -y \
|
||||||
|
curl \
|
||||||
|
sudo \
|
||||||
|
mc \
|
||||||
|
[PACKAGE_1] \
|
||||||
|
[PACKAGE_2] \
|
||||||
|
[PACKAGE_3]
|
||||||
|
msg_ok "Installed Dependencies"
|
||||||
|
|
||||||
|
# Template: MySQL Database
|
||||||
|
msg_info "Setting up Database"
|
||||||
|
DB_NAME=[DB_NAME]
|
||||||
|
DB_USER=[DB_USER]
|
||||||
|
DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
|
||||||
|
$STD mysql -u root -e "CREATE DATABASE $DB_NAME;"
|
||||||
|
$STD mysql -u root -e "CREATE USER '$DB_USER'@'localhost' IDENTIFIED WITH mysql_native_password AS PASSWORD('$DB_PASS');"
|
||||||
|
$STD mysql -u root -e "GRANT ALL ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;"
|
||||||
|
{
|
||||||
|
echo "${APPLICATION} Credentials"
|
||||||
|
echo "Database User: $DB_USER"
|
||||||
|
echo "Database Password: $DB_PASS"
|
||||||
|
echo "Database Name: $DB_NAME"
|
||||||
|
} >> ~/$APP_NAME.creds
|
||||||
|
msg_ok "Set up Database"
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
|
||||||
|
# Setup App
|
||||||
|
msg_info "Setup ${APPLICATION}"
|
||||||
|
RELEASE=$(curl -s https://api.github.com/repos/[REPO]/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
||||||
|
wget -q "https://github.com/[REPO]/archive/refs/tags/${RELEASE}.zip"
|
||||||
|
unzip -q ${RELEASE}.zip
|
||||||
|
mv ${APPLICATION}-${RELEASE}/ /opt/${APPLICATION}
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
echo "${RELEASE}" >/opt/${APPLICATION}_version.txt
|
||||||
|
msg_ok "Setup ${APPLICATION}"
|
||||||
|
|
||||||
|
# Creating Service (if needed)
|
||||||
|
msg_info "Creating Service"
|
||||||
|
cat <<EOF >/etc/systemd/system/${APPLICATION}.service
|
||||||
|
[Unit]
|
||||||
|
Description=${APPLICATION} Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=[START_COMMAND]
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl enable -q --now ${APPLICATION}.service
|
||||||
|
msg_ok "Created Service"
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
msg_info "Cleaning up"
|
||||||
|
rm -f ${RELEASE}.zip
|
||||||
|
$STD apt-get -y autoremove
|
||||||
|
$STD apt-get -y autoclean
|
||||||
|
msg_ok "Cleaned"
|
34
.github/CONTRIBUTOR_AND_GUIDES/json/AppName.json
vendored
Normal file
34
.github/CONTRIBUTOR_AND_GUIDES/json/AppName.json
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "AppName",
|
||||||
|
"slug": "appname",
|
||||||
|
"categories": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"date_created": "DATE CREATED",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": DEFAULT-PORT,
|
||||||
|
"documentation": null,
|
||||||
|
"website": "LINK TO WEBSITE",
|
||||||
|
"logo": "LINK TO LOGO",
|
||||||
|
"description": "Description of the app",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/AppName.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 2,
|
||||||
|
"ram": 2048,
|
||||||
|
"hdd": 4,
|
||||||
|
"os": "debian",
|
||||||
|
"version": "12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
13
.github/CONTRIBUTOR_AND_GUIDES/json/AppName.md
vendored
Normal file
13
.github/CONTRIBUTOR_AND_GUIDES/json/AppName.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# **AppName<span></span>.json Files**
|
||||||
|
|
||||||
|
`AppName.json` files found in the `/json` directory. These files are used to provide informations for the website. For this guide we take `/json/snipeit.json` as example.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [**AppName.json Files**](#appnamejson-files)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [1. JSON Generator](#1-json-generator)
|
||||||
|
|
||||||
|
## 1. JSON Generator
|
||||||
|
|
||||||
|
Use the [JSON Generator](https://community-scripts.github.io/ProxmoxVED/json-editor) to create this file for your application.
|
35
.github/DISCUSSION_TEMPLATE/request-script.yml
vendored
Normal file
35
.github/DISCUSSION_TEMPLATE/request-script.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
title: "[Script request] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Application Name
|
||||||
|
description: Enter the application name.
|
||||||
|
placeholder: "e.g., Home Assistant"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Website
|
||||||
|
description: Official website or github page.
|
||||||
|
placeholder: "e.g., https://www.home-assistant.io/"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Explain what the application does and why it should be added to Proxmox VE Helper-Scripts.
|
||||||
|
placeholder: "e.g., Home Assistant is a popular open-source platform that brings all your smart home devices together in one place. Adding it to Proxmox VE Helper-Scripts would make setup and management on Proxmox easy, letting users quickly get a powerful, self-hosted smart home system up and running."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Due Diligence
|
||||||
|
options:
|
||||||
|
- label: "I have searched existing [scripts](https://community-scripts.github.io/Proxmox/scripts) and found no duplicates."
|
||||||
|
required: true
|
||||||
|
- label: "I have searched existing [discussions](https://github.com/community-scripts/ProxmoxVED/discussions?discussions_q=) and found no duplicate requests."
|
||||||
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "Thanks for submitting your request! The team will review it and reach out if we need more information."
|
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ko_fi: community_scripts
|
||||||
|
github: community_scripts
|
100
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
100
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
name: "🐞 Script Issue Report"
|
||||||
|
description: Report a specific issue with a script. For other inquiries, please use the Discussions section.
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# 🐞 **Script Issue Report**
|
||||||
|
Thank you for taking the time to report an issue! Please provide as much detail as possible to help us address the problem efficiently.
|
||||||
|
|
||||||
|
## ⚠️ **IMPORTANT - READ FIRST**
|
||||||
|
- 🔍 **Search first:** Before submitting, check if the issue has already been reported or resolved in [closed issues](https://github.com/community-scripts/ProxmoxVED/issues?q=is%3Aissue+is%3Aclosed). If found, comment on that issue instead of creating a new one.
|
||||||
|
Alternatively, check the **[Discussions](https://github.com/community-scripts/ProxmoxVED/discussions)** under the *"Announcement"* or *"Guide"* categories for relevant information.
|
||||||
|
- 🛠️ **Supported environments only:** Ensure you are using a default Linux distribution. Custom setups may not be supported.
|
||||||
|
- 🔎 If you encounter `[ERROR] in line 23: exit code *: while executing command "$@" > /dev/null 2>&1`, rerun the script with verbose mode before submitting the issue.
|
||||||
|
- 💡 For general questions, feature requests, or suggestions, use the [Discussions section](https://github.com/community-scripts/ProxmoxVED/discussions).
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: guidelines
|
||||||
|
attributes:
|
||||||
|
label: ✅ Have you read and understood the above guidelines?
|
||||||
|
placeholder: "yes"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: script_name
|
||||||
|
attributes:
|
||||||
|
label: 📜 What is the name of the script you are using?
|
||||||
|
placeholder: "e.g., NextcloudPi, Zigbee2MQTT"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: script_command
|
||||||
|
attributes:
|
||||||
|
label: 📂 What was the exact command used to execute the script?
|
||||||
|
placeholder: "e.g., bash -c \"$(wget -qLO - https://github.com/community-scripts/ProxmoxVED/raw/main/ct/zigbee2mqtt.sh)\" or \"update\""
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: issue_description
|
||||||
|
attributes:
|
||||||
|
label: 📝 Provide a clear and concise description of the issue.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: ⚙️ What settings are you using?
|
||||||
|
options:
|
||||||
|
- label: Default Settings
|
||||||
|
- label: Advanced Settings
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "💡 **Tip:** If you are using Advanced Settings, please test with Default Settings before submitting an issue."
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: linux_distribution
|
||||||
|
attributes:
|
||||||
|
label: 🖥️ Which Linux distribution are you using?
|
||||||
|
options:
|
||||||
|
-
|
||||||
|
- Alpine
|
||||||
|
- Debian 11
|
||||||
|
- Debian 12
|
||||||
|
- Ubuntu 20.04
|
||||||
|
- Ubuntu 22.04
|
||||||
|
- Ubuntu 24.04
|
||||||
|
- Ubuntu 24.10
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps_to_reproduce
|
||||||
|
attributes:
|
||||||
|
label: 🔄 Steps to reproduce the issue.
|
||||||
|
placeholder: "e.g., Step 1: ..., Step 2: ..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: error_output
|
||||||
|
attributes:
|
||||||
|
label: ❌ Paste the full error output (if available).
|
||||||
|
placeholder: "Include any relevant logs or error messages."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional_context
|
||||||
|
attributes:
|
||||||
|
label: 🖼️ Additional context (optional).
|
||||||
|
placeholder: "Include screenshots, code blocks (use triple backticks ```), or any other relevant information."
|
||||||
|
validations:
|
||||||
|
required: false
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 🤔 Questions and Help
|
||||||
|
url: https://github.com/community-scripts/ProxmoxVED/discussions
|
||||||
|
about: For suggestions or questions, please use the Discussions section.
|
||||||
|
- name: 🌟 new Script request
|
||||||
|
url: https://github.com/community-scripts/ProxmoxVED/discussions/new?category=request-script
|
||||||
|
about: For feature/script requests, please use the Discussions section.
|
||||||
|
- name: 💻 Discord
|
||||||
|
url: https://discord.gg/UHrpNWGwkH
|
||||||
|
about: Join our Discord server to chat with other users in the Proxmox Helper Scripts community.
|
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: "✨ Feature Request"
|
||||||
|
description: "Suggest a new feature or enhancement."
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# ✨ **Feature Request**
|
||||||
|
Have an idea for a new feature? Share your thoughts below!
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: feature_summary
|
||||||
|
attributes:
|
||||||
|
label: "🌟 Briefly describe the feature"
|
||||||
|
placeholder: "e.g., Add support for XYZ"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature_description
|
||||||
|
attributes:
|
||||||
|
label: "📝 Detailed description"
|
||||||
|
placeholder: "Explain the feature in detail"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use_case
|
||||||
|
attributes:
|
||||||
|
label: "💡 Why is this useful?"
|
||||||
|
placeholder: "Describe the benefit of this feature"
|
||||||
|
validations:
|
||||||
|
required: true
|
75
.github/ISSUE_TEMPLATE/frontend_report.yml
vendored
Normal file
75
.github/ISSUE_TEMPLATE/frontend_report.yml
vendored
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
name: "🌐 Website Issue Report"
|
||||||
|
description: Report an issue, an optimization request or an documentation issue specifically related to the website.
|
||||||
|
labels: "website"
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**IMPORTANT:** Failure to comply with the following guidelines may result in immediate closure.
|
||||||
|
- Prior to submitting, kindly search the closed issues to check if the problem you are reporting has already been addressed and resolved. If you come across a closed issue that pertains to your problem, please leave a comment on that issue instead of creating a new one.
|
||||||
|
- If the problem is related to a bug in the website, kindly check for browser compatibility and ensure the issue occurs in multiple browsers before submitting.
|
||||||
|
- For suggestions, questions, or feature requests, please use the [Discussions section.](https://github.com/community-scripts/ProxmoxVED/discussions)
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: guidelines
|
||||||
|
attributes:
|
||||||
|
label: Please verify that you have read and understood the guidelines.
|
||||||
|
placeholder: 'yes'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: issue_type
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What type of issue is this?
|
||||||
|
options:
|
||||||
|
-
|
||||||
|
- Bug
|
||||||
|
- Optimization
|
||||||
|
- Documentation
|
||||||
|
- Other
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug_description
|
||||||
|
attributes:
|
||||||
|
label: A clear and concise description of the issue.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: browser
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which browser are you using?
|
||||||
|
options:
|
||||||
|
-
|
||||||
|
- Chrome
|
||||||
|
- Firefox
|
||||||
|
- Safari
|
||||||
|
- Edge
|
||||||
|
- Other
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**If the issue is browser-related**, please provide information on the version and platform (Windows, MacOS, Linux, etc.).
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshot
|
||||||
|
attributes:
|
||||||
|
label: If relevant, including screenshots or a code block can be helpful in clarifying the issue.
|
||||||
|
placeholder: "Code blocks begin and conclude by enclosing the code with three backticks (```) above and below it."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: Please provide detailed steps to reproduce the issue.
|
||||||
|
placeholder: "First do this, then this ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
25
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
25
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: "🛠️ Task / General Request"
|
||||||
|
description: "Request a general task, improvement, or refactor."
|
||||||
|
labels: ["task"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# 🛠️ **Task / General Request**
|
||||||
|
Request a task that isn't a bug or feature request.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: task_summary
|
||||||
|
attributes:
|
||||||
|
label: "📌 Task summary"
|
||||||
|
placeholder: "e.g., Refactor XYZ"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: task_details
|
||||||
|
attributes:
|
||||||
|
label: "📋 Task details"
|
||||||
|
placeholder: "Explain what needs to be done"
|
||||||
|
validations:
|
||||||
|
required: true
|
76
.github/autolabeler-config.json
vendored
Normal file
76
.github/autolabeler-config.json
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"new script": [
|
||||||
|
{
|
||||||
|
"fileStatus": "added",
|
||||||
|
"includeGlobs": ["ct/**", "install/**", "misc/**", "turnkey/**", "vm/**"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"update script": [
|
||||||
|
{
|
||||||
|
"fileStatus": "modified",
|
||||||
|
"includeGlobs": ["ct/**", "install/**", "misc/**", "turnkey/**", "vm/**"],
|
||||||
|
"excludeGlobs": ["misc/build.func", "misc/install.func", "misc/api.func"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"delete script": [
|
||||||
|
{
|
||||||
|
"fileStatus": "removed",
|
||||||
|
"includeGlobs": ["ct/**", "install/**", "misc/**", "turnkey/**", "vm/**"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"maintenance": [
|
||||||
|
{
|
||||||
|
"fileStatus": null,
|
||||||
|
"includeGlobs": ["*.md", ".github/**", "misc/*.func", "ct/create_lxc.sh", "api/**"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"core": [
|
||||||
|
{
|
||||||
|
"fileStatus": null,
|
||||||
|
"includeGlobs": ["misc/*.func", "ct/create_lxc.sh"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"website": [
|
||||||
|
{
|
||||||
|
"fileStatus": null,
|
||||||
|
"includeGlobs": ["frontend/**", "json/**"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": [
|
||||||
|
{
|
||||||
|
"fileStatus": null,
|
||||||
|
"includeGlobs": ["api/**", "misc/api.func"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"github": [
|
||||||
|
{
|
||||||
|
"fileStatus": null,
|
||||||
|
"includeGlobs": [".github/**"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"json": [
|
||||||
|
{
|
||||||
|
"fileStatus": "modified",
|
||||||
|
"includeGlobs": ["json/**"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"high risk": [
|
||||||
|
{
|
||||||
|
"fileStatus": null,
|
||||||
|
"includeGlobs": ["misc/build.func", "misc/install.func", "ct/create_lxc.sh"],
|
||||||
|
"excludeGlobs": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
}
|
97
.github/changelog-pr-config.json
vendored
Normal file
97
.github/changelog-pr-config.json
vendored
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "🆕 New Scripts",
|
||||||
|
"labels": ["new script"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "🚀 Updated Scripts",
|
||||||
|
"labels": ["update script"],
|
||||||
|
"subCategories": [
|
||||||
|
{
|
||||||
|
"title": "🐞 Bug Fixes",
|
||||||
|
"labels": ["bugfix"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "✨ New Features",
|
||||||
|
"labels": ["feature"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "💥 Breaking Changes",
|
||||||
|
"labels": ["breaking change"],
|
||||||
|
"notes" : []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "🧰 Maintenance",
|
||||||
|
"labels": ["maintenance"],
|
||||||
|
"subCategories": [
|
||||||
|
{
|
||||||
|
"title": "🐞 Bug Fixes",
|
||||||
|
"labels": ["bugfix"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "✨ New Features",
|
||||||
|
"labels": ["feature"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "💥 Breaking Changes",
|
||||||
|
"labels": ["breaking change"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "📡 API",
|
||||||
|
"labels": ["api"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "💾 Core",
|
||||||
|
"labels": ["core"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "📂 Github",
|
||||||
|
"labels": ["github"],
|
||||||
|
"notes" : []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "🌐 Website",
|
||||||
|
"labels": ["website"],
|
||||||
|
"subCategories": [
|
||||||
|
{
|
||||||
|
"title": "🐞 Bug Fixes",
|
||||||
|
"labels": ["bugfix"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "✨ New Features",
|
||||||
|
"labels": ["feature"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "💥 Breaking Changes",
|
||||||
|
"labels": ["breaking change"],
|
||||||
|
"notes" : []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "📝 Script Information",
|
||||||
|
"labels": ["json"],
|
||||||
|
"notes" : []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "❔ Unlabelled",
|
||||||
|
"labels": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "💥 Breaking Changes",
|
||||||
|
"labels": ["breaking change"]
|
||||||
|
}
|
||||||
|
]
|
25
.github/pull_request_template.md
vendored
Normal file
25
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
## ✍️ Description
|
||||||
|
<!-- Provide a clear and concise description of your changes. -->
|
||||||
|
|
||||||
|
## 🔗 Related PR / Discussion / Issue
|
||||||
|
|
||||||
|
Link: #
|
||||||
|
|
||||||
|
## ✅ Prerequisites
|
||||||
|
|
||||||
|
Before this PR can be reviewed, the following must be completed:
|
||||||
|
|
||||||
|
- [] **Self-review performed** – Code follows established patterns and conventions.
|
||||||
|
- [] **Testing performed** – Changes have been thoroughly tested and verified.
|
||||||
|
|
||||||
|
## 🛠️ Type of Change
|
||||||
|
|
||||||
|
Select all that apply:
|
||||||
|
|
||||||
|
- [] 🆕 **New script** – A fully functional and tested script or script set.
|
||||||
|
- [] 🐞 **Bug fix** – Resolves an issue without breaking functionality.
|
||||||
|
- [] ✨ **New feature** – Adds new, non-breaking functionality.
|
||||||
|
- [] 💥 **Breaking change** – Alters existing functionality in a way that may require updates.
|
||||||
|
|
||||||
|
## 📋 Additional Information (optional)
|
||||||
|
<!-- Provide extra context, screenshots, or references if needed. -->
|
68
.github/runner/docker/gh-runner-self.dockerfile
vendored
Normal file
68
.github/runner/docker/gh-runner-self.dockerfile
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy as build
|
||||||
|
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG DOCKER_VERSION=27.5.1
|
||||||
|
ARG BUILDX_VERSION=0.20.1
|
||||||
|
ARG RUNNER_ARCH="x64"
|
||||||
|
|
||||||
|
RUN apt update -y && apt install sudo curl unzip -y
|
||||||
|
|
||||||
|
WORKDIR /actions-runner
|
||||||
|
|
||||||
|
RUN RUNNER_VERSION=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | grep "tag_name" | head -n 1 | awk '{print substr($2, 3, length($2)-4)}') \
|
||||||
|
&& curl -f -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
|
||||||
|
&& tar xzf ./runner.tar.gz \
|
||||||
|
&& rm runner.tar.gz
|
||||||
|
|
||||||
|
RUN RUNNER_CONTAINER_HOOKS_VERSION=$(curl -s https://api.github.com/repos/actions/runner-container-hooks/releases/latest | grep "tag_name" | head -n 1 | awk '{print substr($2, 3, length($2)-4)}') \
|
||||||
|
&& curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v${RUNNER_CONTAINER_HOOKS_VERSION}/actions-runner-hooks-k8s-${RUNNER_CONTAINER_HOOKS_VERSION}.zip \
|
||||||
|
&& unzip ./runner-container-hooks.zip -d ./k8s \
|
||||||
|
&& rm runner-container-hooks.zip
|
||||||
|
|
||||||
|
RUN export RUNNER_ARCH=${TARGETARCH} \
|
||||||
|
&& if [ "$RUNNER_ARCH" = "amd64" ]; then export DOCKER_ARCH=x86_64 ; fi \
|
||||||
|
&& if [ "$RUNNER_ARCH" = "arm64" ]; then export DOCKER_ARCH=aarch64 ; fi \
|
||||||
|
&& curl -fLo docker.tgz https://download.docker.com/${TARGETOS}/static/stable/${DOCKER_ARCH}/docker-${DOCKER_VERSION}.tgz \
|
||||||
|
&& tar zxvf docker.tgz \
|
||||||
|
&& rm -rf docker.tgz \
|
||||||
|
&& mkdir -p /usr/local/lib/docker/cli-plugins \
|
||||||
|
&& curl -fLo /usr/local/lib/docker/cli-plugins/docker-buildx \
|
||||||
|
"https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" \
|
||||||
|
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV RUNNER_MANUALLY_TRAP_SIG=1
|
||||||
|
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
|
||||||
|
ENV ImageOS=ubuntu22
|
||||||
|
|
||||||
|
RUN apt update -y \
|
||||||
|
&& apt install -y --no-install-recommends sudo lsb-release gpg-agent software-properties-common curl jq unzip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN add-apt-repository ppa:git-core/ppa \
|
||||||
|
&& apt update -y \
|
||||||
|
&& apt install -y git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
|
||||||
|
&& groupadd docker --gid 123 \
|
||||||
|
&& usermod -aG sudo runner \
|
||||||
|
&& usermod -aG docker runner \
|
||||||
|
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers \
|
||||||
|
&& echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers
|
||||||
|
|
||||||
|
# Install own dependencies in final image
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& apt-get install -y gh jq git
|
||||||
|
|
||||||
|
WORKDIR /home/runner
|
||||||
|
|
||||||
|
COPY --chown=runner:docker --from=build /actions-runner .
|
||||||
|
COPY --from=build /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-buildx
|
||||||
|
RUN install -o root -g root -m 755 docker/* /usr/bin/ && rm -rf docker
|
||||||
|
|
||||||
|
USER runner
|
106
.github/workflows/auto-update-app-headers.yml
vendored
Normal file
106
.github/workflows/auto-update-app-headers.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
name: Auto Update .app-files
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'ct/**.sh'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-app-files:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Generate a token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
# Step 1: Checkout repository
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Step 2: Disable file mode changes detection
|
||||||
|
- name: Disable file mode changes
|
||||||
|
run: git config core.fileMode false
|
||||||
|
|
||||||
|
# Step 3: Set up Git user for committing changes
|
||||||
|
- name: Set up Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "GitHub Actions"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# Step 4: Install figlet
|
||||||
|
- name: Install figlet
|
||||||
|
run: sudo apt-get install -y figlet
|
||||||
|
|
||||||
|
# Step 5: Run the updated generate-app-files.sh script
|
||||||
|
- name: Run generate-app-files.sh
|
||||||
|
run: |
|
||||||
|
chmod +x .github/workflows/scripts/generate-app-headers.sh
|
||||||
|
.github/workflows/scripts/generate-app-headers.sh
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Step 6: Check if there are any changes
|
||||||
|
- name: Check if there are any changes
|
||||||
|
run: |
|
||||||
|
echo "Checking for changes..."
|
||||||
|
git add -A # Untracked Dateien aufnehmen
|
||||||
|
git status
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes detected."
|
||||||
|
echo "changed=false" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "Changes detected:"
|
||||||
|
git diff --stat --cached
|
||||||
|
echo "changed=true" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 7: Commit and create PR if changes exist
|
||||||
|
- name: Commit and create PR if changes exist
|
||||||
|
if: env.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git commit -m "Update .app files"
|
||||||
|
git checkout -b pr-update-app-files
|
||||||
|
git push origin pr-update-app-files --force
|
||||||
|
gh pr create --title "[core] update .app files" \
|
||||||
|
--body "This PR is auto-generated by a GitHub Action to update the .app files." \
|
||||||
|
--head pr-update-app-files \
|
||||||
|
--base main \
|
||||||
|
--label "automated pr"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Approve pull request
|
||||||
|
if: env.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(gh pr list --head "pr-update-app-files" --json number --jq '.[].number')
|
||||||
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
|
gh pr review $PR_NUMBER --approve
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Re-approve pull request after update
|
||||||
|
if: env.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(gh pr list --head "pr-update-app-files" --json number --jq '.[].number')
|
||||||
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
|
gh pr review $PR_NUMBER --approve
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 8: Output success message when no changes
|
||||||
|
- name: No changes detected
|
||||||
|
if: env.changed == 'false'
|
||||||
|
run: echo "No changes to commit. Workflow completed successfully."
|
90
.github/workflows/autolabeler.yml
vendored
Normal file
90
.github/workflows/autolabeler.yml
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
name: Auto Label Pull Requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autolabeler:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
env:
|
||||||
|
CONFIG_PATH: .github/autolabeler-config.json
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install minimatch
|
||||||
|
run: npm install minimatch
|
||||||
|
|
||||||
|
- name: Label PR based on file changes and PR template
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const { minimatch } = require('minimatch');
|
||||||
|
|
||||||
|
const configPath = path.resolve(process.env.CONFIG_PATH);
|
||||||
|
const fileContent = await fs.readFile(configPath, 'utf-8');
|
||||||
|
const autolabelerConfig = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const prBody = context.payload.pull_request.body.toLowerCase();
|
||||||
|
|
||||||
|
let labelsToAdd = new Set();
|
||||||
|
|
||||||
|
const prListFilesResponse = await github.rest.pulls.listFiles({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: prNumber,
|
||||||
|
});
|
||||||
|
const prFiles = prListFilesResponse.data;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Apply labels based on file changes
|
||||||
|
for (const [label, rules] of Object.entries(autolabelerConfig)) {
|
||||||
|
const shouldAddLabel = prFiles.some((prFile) => {
|
||||||
|
return rules.some((rule) => {
|
||||||
|
const isFileStatusMatch = rule.fileStatus ? rule.fileStatus === prFile.status : true;
|
||||||
|
const isIncludeGlobMatch = rule.includeGlobs.some((glob) => minimatch(prFile.filename, glob));
|
||||||
|
const isExcludeGlobMatch = rule.excludeGlobs.some((glob) => minimatch(prFile.filename, glob));
|
||||||
|
|
||||||
|
return isFileStatusMatch && isIncludeGlobMatch && !isExcludeGlobMatch;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldAddLabel) {
|
||||||
|
labelsToAdd.add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const templateLabelMappings = {
|
||||||
|
"🐞 **Bug fix**": "bugfix",
|
||||||
|
"✨ **New feature**": "feature",
|
||||||
|
"💥 **Breaking change**": "breaking change",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [checkbox, label] of Object.entries(templateLabelMappings)) {
|
||||||
|
const escapedCheckbox = checkbox.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
||||||
|
const regex = new RegExp(`- \\[(x|X)\\]\\s*.*${escapedCheckbox}`, "i");
|
||||||
|
const match = prBody.match(regex);
|
||||||
|
if (match) {
|
||||||
|
console.log(`Match: ${match}`);
|
||||||
|
labelsToAdd.add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Labels to add: ${Array.from(labelsToAdd).join(", ")}`);
|
||||||
|
|
||||||
|
if (labelsToAdd.size > 0) {
|
||||||
|
console.log(`Adding labels ${Array.from(labelsToAdd).join(", ")} to PR ${prNumber}`);
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
labels: Array.from(labelsToAdd),
|
||||||
|
});
|
||||||
|
}
|
60
.github/workflows/backup/check_and_update_json_date.yml
vendored
Normal file
60
.github/workflows/backup/check_and_update_json_date.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
name: Update date_created in JSON files
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Dieser Trigger wird für das Öffnen von PRs sowie für das Aktualisieren von offenen PRs verwendet
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
schedule:
|
||||||
|
# Dieser Trigger wird 4x am Tag ausgelöst, um sicherzustellen, dass das Datum aktualisiert wird
|
||||||
|
- cron: "0 0,6,12,18 * * *" # Führt alle 6 Stunden aus
|
||||||
|
workflow_dispatch: # Manuelle Ausführung des Workflows möglich
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-date:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install yq
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y yq
|
||||||
|
|
||||||
|
- name: Set the current date
|
||||||
|
id: set_date
|
||||||
|
run: echo "TODAY=$(date -u +%Y-%m-%d)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check for changes in PR
|
||||||
|
run: |
|
||||||
|
# Hole den PR-Branch
|
||||||
|
PR_BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||||
|
git fetch origin $PR_BRANCH
|
||||||
|
|
||||||
|
# Liste alle JSON-Dateien im PR auf, die geändert wurden
|
||||||
|
CHANGED_JSON_FILES=$(git diff --name-only origin/main...$PR_BRANCH | grep '.json')
|
||||||
|
|
||||||
|
if [ -z "$CHANGED_JSON_FILES" ]; then
|
||||||
|
echo "No JSON files changed in this PR."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gehe alle geänderten JSON-Dateien durch und aktualisiere das Datum
|
||||||
|
for file in $CHANGED_JSON_FILES; do
|
||||||
|
echo "Updating date_created in $file"
|
||||||
|
# Setze das aktuelle Datum
|
||||||
|
yq eval ".date_created = \"${{ env.TODAY }}\"" -i "$file"
|
||||||
|
git add "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
run: |
|
||||||
|
# Prüfe, ob es Änderungen gibt und committe sie
|
||||||
|
git config user.name "json-updater-bot"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
git commit -m "Update date_created to ${{ env.TODAY }}" || echo "No changes to commit"
|
||||||
|
|
||||||
|
# Push zurück in den PR-Branch
|
||||||
|
git push origin $PR_BRANCH
|
60
.github/workflows/backup/shellcheck.yml
vendored
Normal file
60
.github/workflows/backup/shellcheck.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
name: Shellcheck
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "5 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shellcheck:
|
||||||
|
name: Shellcheck
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
**.sh
|
||||||
|
|
||||||
|
- name: Download ShellCheck
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_VERSION: "v0.10.0"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "${{ runner.os }}" == "macOS" ]]; then
|
||||||
|
osvariant="darwin"
|
||||||
|
else
|
||||||
|
osvariant="linux"
|
||||||
|
fi
|
||||||
|
|
||||||
|
baseurl="https://github.com/koalaman/shellcheck/releases/download"
|
||||||
|
curl -Lso "${{ github.workspace }}/sc.tar.xz" \
|
||||||
|
"${baseurl}/${INPUT_VERSION}/shellcheck-${INPUT_VERSION}.${osvariant}.x86_64.tar.xz"
|
||||||
|
|
||||||
|
tar -xf "${{ github.workspace }}/sc.tar.xz" -C "${{ github.workspace }}"
|
||||||
|
mv "${{ github.workspace }}/shellcheck-${INPUT_VERSION}/shellcheck" \
|
||||||
|
"${{ github.workspace }}/shellcheck"
|
||||||
|
|
||||||
|
- name: Verify ShellCheck binary
|
||||||
|
run: |
|
||||||
|
ls -l "${{ github.workspace }}/shellcheck"
|
||||||
|
|
||||||
|
- name: Display ShellCheck version
|
||||||
|
run: |
|
||||||
|
"${{ github.workspace }}/shellcheck" --version
|
||||||
|
|
||||||
|
- name: Run ShellCheck
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
env:
|
||||||
|
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
|
||||||
|
run: |
|
||||||
|
echo "${ALL_CHANGED_FILES}" | xargs "${{ github.workspace }}/shellcheck"
|
88
.github/workflows/backup/update_json_date.yml.bak
vendored
Normal file
88
.github/workflows/backup/update_json_date.yml.bak
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
name: Auto Update JSON-Date
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-json-dates:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Generate a token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Full history for proper detection
|
||||||
|
|
||||||
|
- name: Set up Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "GitHub Actions"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Find JSON files with incorrect date_created
|
||||||
|
id: find_wrong_json
|
||||||
|
run: |
|
||||||
|
TODAY=$(date -u +"%Y-%m-%d")
|
||||||
|
> incorrect_json_files.txt
|
||||||
|
|
||||||
|
for FILE in json/*.json; do
|
||||||
|
if [[ -f "$FILE" ]]; then
|
||||||
|
DATE_IN_JSON=$(jq -r '.date_created' "$FILE" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ "$DATE_IN_JSON" != "$TODAY" ]]; then
|
||||||
|
echo "$FILE" >> incorrect_json_files.txt
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -s incorrect_json_files.txt ]]; then
|
||||||
|
echo "CHANGED=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "CHANGED=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run update script
|
||||||
|
if: env.CHANGED == 'true'
|
||||||
|
run: |
|
||||||
|
chmod +x .github/workflows/scripts/update-json.sh
|
||||||
|
while read -r FILE; do
|
||||||
|
.github/workflows/scripts/update-json.sh "$FILE"
|
||||||
|
done < incorrect_json_files.txt
|
||||||
|
|
||||||
|
- name: Commit and create PR if changes exist
|
||||||
|
if: env.CHANGED == 'true'
|
||||||
|
run: |
|
||||||
|
git add json/*.json
|
||||||
|
git commit -m "Auto-update date_created in incorrect JSON files"
|
||||||
|
git checkout -b pr-fix-json-dates
|
||||||
|
git push origin pr-fix-json-dates --force
|
||||||
|
gh pr create --title "[core] Fix incorrect JSON date_created fields" \
|
||||||
|
--body "This PR is auto-generated to fix incorrect `date_created` fields in JSON files." \
|
||||||
|
--head pr-fix-json-dates \
|
||||||
|
--base main \
|
||||||
|
--label "automated pr"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Approve pull request
|
||||||
|
if: env.CHANGED == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(gh pr list --head "pr-fix-json-dates" --json number --jq '.[].number')
|
||||||
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
|
gh pr review $PR_NUMBER --approve
|
||||||
|
fi
|
133
.github/workflows/backup/validate-formatting.yaml.bak
vendored
Normal file
133
.github/workflows/backup/validate-formatting.yaml.bak
vendored
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
name: Validate script formatting
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request_target:
|
||||||
|
paths:
|
||||||
|
- "**/*.sh"
|
||||||
|
- "**/*.func"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shfmt:
|
||||||
|
name: Check changed files
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Get pull request information
|
||||||
|
if: github.event_name == 'pull_request_target'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
id: pr
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: pullRequest } = await github.rest.pulls.get({
|
||||||
|
...context.repo,
|
||||||
|
pull_number: context.payload.pull_request.number,
|
||||||
|
});
|
||||||
|
return pullRequest;
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Ensure the full history is fetched for accurate diffing
|
||||||
|
ref: ${{ github.event_name == 'pull_request_target' && fromJSON(steps.pr.outputs.result).merge_commit_sha || '' }}
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
run: |
|
||||||
|
if ${{ github.event_name == 'pull_request_target' }}; then
|
||||||
|
echo "files=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ steps.pr.outputs.result && fromJSON(steps.pr.outputs.result).merge_commit_sha }} | grep -E '\.(sh|func)$' | xargs)" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | grep -E '\.(sh|func)$' | xargs)" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
if: steps.changed-files.outputs.files != ''
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
|
||||||
|
- name: Install shfmt
|
||||||
|
if: steps.changed-files.outputs.files != ''
|
||||||
|
run: |
|
||||||
|
go install mvdan.cc/sh/v3/cmd/shfmt@latest
|
||||||
|
echo "$GOPATH/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Run shfmt
|
||||||
|
if: steps.changed-files.outputs.files != ''
|
||||||
|
id: shfmt
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
|
||||||
|
|
||||||
|
shfmt_output=$(shfmt -d ${{ steps.changed-files.outputs.files }})
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "diff=\"$(echo -n "$shfmt_output" | base64 -w 0)\"" >> $GITHUB_OUTPUT
|
||||||
|
printf "%s" "$shfmt_output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post comment with results
|
||||||
|
if: always() && steps.changed-files.outputs.files != '' && github.event_name == 'pull_request_target'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const result = "${{ job.status }}" === "success" ? "success" : "failure";
|
||||||
|
const diff = Buffer.from(
|
||||||
|
${{ steps.shfmt.outputs.diff }},
|
||||||
|
"base64",
|
||||||
|
).toString();
|
||||||
|
const issueNumber = context.payload.pull_request
|
||||||
|
? context.payload.pull_request.number
|
||||||
|
: null;
|
||||||
|
const commentIdentifier = "validate-formatting";
|
||||||
|
let newCommentBody = `<!-- ${commentIdentifier}-start -->\n### Script formatting\n\n`;
|
||||||
|
|
||||||
|
if (result === "failure") {
|
||||||
|
newCommentBody +=
|
||||||
|
`:x: We found issues in the formatting of the following changed files:\n\n\`\`\`diff\n${diff}\n\`\`\`\n`;
|
||||||
|
} else {
|
||||||
|
newCommentBody += `:rocket: All changed shell scripts are formatted correctly!\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
newCommentBody += `\n\n<!-- ${commentIdentifier}-end -->`;
|
||||||
|
|
||||||
|
if (issueNumber) {
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
...context.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingComment = comments.find(
|
||||||
|
(comment) => comment.user.login === "github-actions[bot]",
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingComment) {
|
||||||
|
if (existingComment.body.includes(commentIdentifier)) {
|
||||||
|
const re = new RegExp(
|
||||||
|
String.raw`<!-- ${commentIdentifier}-start -->[\s\S]*?<!-- ${commentIdentifier}-end -->`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
newCommentBody = existingComment.body.replace(re, newCommentBody);
|
||||||
|
} else {
|
||||||
|
newCommentBody = existingComment.body + "\n\n---\n\n" + newCommentBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
...context.repo,
|
||||||
|
comment_id: existingComment.id,
|
||||||
|
body: newCommentBody,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
...context.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body: newCommentBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
234
.github/workflows/backup/validate-scripts.yml.bak
vendored
Normal file
234
.github/workflows/backup/validate-scripts.yml.bak
vendored
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
name: Validate scripts
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request_target:
|
||||||
|
paths:
|
||||||
|
- "ct/*.sh"
|
||||||
|
- "install/*.sh"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-scripts:
|
||||||
|
name: Check changed files
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Debug event payload
|
||||||
|
run: |
|
||||||
|
echo "Event name: ${{ github.event_name }}"
|
||||||
|
echo "Payload: $(cat $GITHUB_EVENT_PATH)"
|
||||||
|
|
||||||
|
- name: Get pull request information
|
||||||
|
if: github.event_name == 'pull_request_target'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
id: pr
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: pullRequest } = await github.rest.pulls.get({
|
||||||
|
...context.repo,
|
||||||
|
pull_number: context.payload.pull_request.number,
|
||||||
|
});
|
||||||
|
return pullRequest;
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event_name == 'pull_request_target' && fromJSON(steps.pr.outputs.result).merge_commit_sha || '' }}
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "pull_request_target" ]; then
|
||||||
|
echo "files=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ steps.pr.outputs.result && fromJSON(steps.pr.outputs.result).merge_commit_sha }} | grep -E '\.(sh|func)$' | xargs)" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | grep -E '\.(sh|func)$' | xargs)" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check build.func line
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: build-func
|
||||||
|
run: |
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in ${{ steps.changed-files.outputs.files }}; do
|
||||||
|
if [[ "$FILE" == ct/* ]] && [[ $(sed -n '2p' "$FILE") != "source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)" ]]; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "Build.func line missing or incorrect in files:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check executable permissions
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: check-executable
|
||||||
|
run: |
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in ${{ steps.changed-files.outputs.files }}; do
|
||||||
|
if [[ ! -x "$FILE" ]]; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "Files not executable:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check copyright
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: check-copyright
|
||||||
|
run: |
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in ${{ steps.changed-files.outputs.files }}; do
|
||||||
|
if ! sed -n '3p' "$FILE" | grep -qE "^# Copyright \(c\) [0-9]{4}(-[0-9]{4})? (tteck \| community-scripts ORG|community-scripts ORG|tteck)$"; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "Copyright header missing or not on line 3 in files:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check author
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: check-author
|
||||||
|
run: |
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in ${{ steps.changed-files.outputs.files }}; do
|
||||||
|
if ! sed -n '4p' "$FILE" | grep -qE "^# Author: .+"; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "Author header missing or invalid on line 4 in files:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check license
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: check-license
|
||||||
|
run: |
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in ${{ steps.changed-files.outputs.files }}; do
|
||||||
|
if [[ "$(sed -n '5p' "$FILE")" != "# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE" ]]; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "License header missing or not on line 5 in files:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check source
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: check-source
|
||||||
|
run: |
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in ${{ steps.changed-files.outputs.files }}; do
|
||||||
|
if ! sed -n '6p' "$FILE" | grep -qE "^# Source: .+"; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "Source header missing or not on line 6 in files:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post results and comment
|
||||||
|
if: always() && steps.changed-files.outputs.files != '' && github.event_name == 'pull_request_target'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const result = '${{ job.status }}' === 'success' ? 'success' : 'failure';
|
||||||
|
const nonCompliantFiles = {
|
||||||
|
'Invalid build.func source': "${{ steps.build-func.outputs.files || '' }}",
|
||||||
|
'Not executable': "${{ steps.check-executable.outputs.files || '' }}",
|
||||||
|
'Copyright header line missing or invalid': "${{ steps.check-copyright.outputs.files || '' }}",
|
||||||
|
'Author header line missing or invalid': "${{ steps.check-author.outputs.files || '' }}",
|
||||||
|
'License header line missing or invalid': "${{ steps.check-license.outputs.files || '' }}",
|
||||||
|
'Source header line missing or invalid': "${{ steps.check-source.outputs.files || '' }}"
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueNumber = context.payload.pull_request ? context.payload.pull_request.number : null;
|
||||||
|
const commentIdentifier = 'validate-scripts';
|
||||||
|
let newCommentBody = `<!-- ${commentIdentifier}-start -->\n### Script validation\n\n`;
|
||||||
|
|
||||||
|
if (result === 'failure') {
|
||||||
|
newCommentBody += ':x: We found issues in the following changed files:\n\n';
|
||||||
|
for (const [check, files] of Object.entries(nonCompliantFiles)) {
|
||||||
|
if (files) {
|
||||||
|
newCommentBody += `**${check}:**\n`;
|
||||||
|
files.trim().split(' ').forEach(file => {
|
||||||
|
newCommentBody += `- ${file}: ${check}\n`;
|
||||||
|
});
|
||||||
|
newCommentBody += `\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newCommentBody += `:rocket: All changed shell scripts passed validation!\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
newCommentBody += `\n\n<!-- ${commentIdentifier}-end -->`;
|
||||||
|
|
||||||
|
if (issueNumber) {
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
...context.repo,
|
||||||
|
issue_number: issueNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingComment = comments.find(comment =>
|
||||||
|
comment.body.includes(`<!-- ${commentIdentifier}-start -->`) &&
|
||||||
|
comment.user.login === 'github-actions[bot]'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingComment) {
|
||||||
|
const re = new RegExp(String.raw`<!-- ${commentIdentifier}-start -->[\\s\\S]*?<!-- ${commentIdentifier}-end -->`, "m");
|
||||||
|
newCommentBody = existingComment.body.replace(re, newCommentBody);
|
||||||
|
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
...context.repo,
|
||||||
|
comment_id: existingComment.id,
|
||||||
|
body: newCommentBody
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
...context.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body: newCommentBody
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
226
.github/workflows/changelog-pr.yml
vendored
Normal file
226
.github/workflows/changelog-pr.yml
vendored
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
name: Create Changelog Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-changelog-pull-request:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
env:
|
||||||
|
CONFIG_PATH: .github/changelog-pr-config.json
|
||||||
|
BRANCH_NAME: github-action-update-changelog
|
||||||
|
AUTOMATED_PR_LABEL: "automated pr"
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Generate a token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get latest dates in changelog
|
||||||
|
run: |
|
||||||
|
DATES=$(grep -E '^## [0-9]{4}-[0-9]{2}-[0-9]{2}' CHANGELOG.md | head -n 2 | awk '{print $2}')
|
||||||
|
|
||||||
|
LATEST_DATE=$(echo "$DATES" | sed -n '1p')
|
||||||
|
SECOND_LATEST_DATE=$(echo "$DATES" | sed -n '2p')
|
||||||
|
TODAY=$(date -u +%Y-%m-%d)
|
||||||
|
|
||||||
|
echo "TODAY=$TODAY" >> $GITHUB_ENV
|
||||||
|
if [[ "$LATEST_DATE" == "$TODAY" ]]; then
|
||||||
|
echo "LATEST_DATE=$SECOND_LATEST_DATE" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "LATEST_DATE=$LATEST_DATE" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get categorized pull requests
|
||||||
|
id: get-categorized-prs
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const configPath = path.resolve(process.env.CONFIG_PATH);
|
||||||
|
const fileContent = await fs.readFile(configPath, 'utf-8');
|
||||||
|
const changelogConfig = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
const categorizedPRs = changelogConfig.map(obj => ({
|
||||||
|
...obj,
|
||||||
|
notes: [],
|
||||||
|
subCategories: obj.subCategories ?? (
|
||||||
|
obj.labels.includes("update script") ? [
|
||||||
|
{ title: "🐞 Bug Fixes", labels: ["bugfix"], notes: [] },
|
||||||
|
{ title: "✨ New Features", labels: ["feature"], notes: [] },
|
||||||
|
{ title: "💥 Breaking Changes", labels: ["breaking change"], notes: [] }
|
||||||
|
] :
|
||||||
|
obj.labels.includes("maintenance") ? [
|
||||||
|
{ title: "🐞 Bug Fixes", labels: ["bugfix"], notes: [] },
|
||||||
|
{ title: "✨ New Features", labels: ["feature"], notes: [] },
|
||||||
|
{ title: "💥 Breaking Changes", labels: ["breaking change"], notes: [] },
|
||||||
|
{ title: "📡 API", labels: ["api"], notes: [] },
|
||||||
|
{ title: "Github", labels: ["github"], notes: [] }
|
||||||
|
] :
|
||||||
|
obj.labels.includes("website") ? [
|
||||||
|
{ title: "🐞 Bug Fixes", labels: ["bugfix"], notes: [] },
|
||||||
|
{ title: "✨ New Features", labels: ["feature"], notes: [] },
|
||||||
|
{ title: "💥 Breaking Changes", labels: ["breaking change"], notes: [] },
|
||||||
|
{ title: "Script Information", labels: ["json"], notes: [] }
|
||||||
|
] : []
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const latestDateInChangelog = new Date(process.env.LATEST_DATE);
|
||||||
|
latestDateInChangelog.setUTCHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const { data: pulls } = await github.rest.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
base: "main",
|
||||||
|
state: "closed",
|
||||||
|
sort: "updated",
|
||||||
|
direction: "desc",
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
pulls.filter(pr =>
|
||||||
|
pr.merged_at &&
|
||||||
|
new Date(pr.merged_at) > latestDateInChangelog &&
|
||||||
|
!pr.labels.some(label =>
|
||||||
|
["invalid", "wontdo", process.env.AUTOMATED_PR_LABEL].includes(label.name.toLowerCase())
|
||||||
|
)
|
||||||
|
).forEach(pr => {
|
||||||
|
|
||||||
|
const prLabels = pr.labels.map(label => label.name.toLowerCase());
|
||||||
|
const prNote = `- ${pr.title} [@${pr.user.login}](https://github.com/${pr.user.login}) ([#${pr.number}](${pr.html_url}))`;
|
||||||
|
|
||||||
|
const updateScriptsCategory = categorizedPRs.find(category =>
|
||||||
|
category.labels.some(label => prLabels.includes(label))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateScriptsCategory) {
|
||||||
|
|
||||||
|
const subCategory = updateScriptsCategory.subCategories.find(sub =>
|
||||||
|
sub.labels.some(label => prLabels.includes(label))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subCategory) {
|
||||||
|
subCategory.notes.push(prNote);
|
||||||
|
} else {
|
||||||
|
updateScriptsCategory.notes.push(prNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(categorizedPRs, null, 2));
|
||||||
|
|
||||||
|
return categorizedPRs;
|
||||||
|
|
||||||
|
|
||||||
|
- name: Update CHANGELOG.md
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const today = process.env.TODAY;
|
||||||
|
const latestDateInChangelog = process.env.LATEST_DATE;
|
||||||
|
const changelogPath = path.resolve('CHANGELOG.md');
|
||||||
|
const categorizedPRs = ${{ steps.get-categorized-prs.outputs.result }};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(categorizedPRs, null, 2));
|
||||||
|
|
||||||
|
|
||||||
|
let newReleaseNotes = `## ${today}\n\n`;
|
||||||
|
for (const { title, notes, subCategories } of categorizedPRs) {
|
||||||
|
const hasSubcategories = subCategories && subCategories.length > 0;
|
||||||
|
const hasMainNotes = notes.length > 0;
|
||||||
|
const hasSubNotes = hasSubcategories && subCategories.some(sub => sub.notes && sub.notes.length > 0);
|
||||||
|
|
||||||
|
|
||||||
|
if (hasMainNotes || hasSubNotes) {
|
||||||
|
newReleaseNotes += `### ${title}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMainNotes) {
|
||||||
|
newReleaseNotes += ` ${notes.join("\n")}\n\n`;
|
||||||
|
}
|
||||||
|
if (hasSubcategories) {
|
||||||
|
for (const { title: subTitle, notes: subNotes } of subCategories) {
|
||||||
|
if (subNotes && subNotes.length > 0) {
|
||||||
|
newReleaseNotes += ` - #### ${subTitle}\n\n`;
|
||||||
|
newReleaseNotes += ` ${subNotes.join("\n ")}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelogContent = await fs.readFile(changelogPath, 'utf-8');
|
||||||
|
const changelogIncludesTodaysReleaseNotes = changelogContent.includes(`\n## ${today}`);
|
||||||
|
|
||||||
|
const regex = changelogIncludesTodaysReleaseNotes
|
||||||
|
? new RegExp(`## ${today}.*(?=## ${latestDateInChangelog})`, "gs")
|
||||||
|
: new RegExp(`(?=## ${latestDateInChangelog})`, "gs");
|
||||||
|
|
||||||
|
const newChangelogContent = changelogContent.replace(regex, newReleaseNotes);
|
||||||
|
await fs.writeFile(changelogPath, newChangelogContent);
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: verify-diff
|
||||||
|
run: |
|
||||||
|
git diff --quiet . || echo "changed=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
if: env.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add CHANGELOG.md
|
||||||
|
git commit -m "Update CHANGELOG.md"
|
||||||
|
git checkout -b $BRANCH_NAME || git checkout $BRANCH_NAME
|
||||||
|
git push origin $BRANCH_NAME --force
|
||||||
|
|
||||||
|
- name: Create pull request if not exists
|
||||||
|
if: env.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
PR_EXISTS=$(gh pr list --head "${BRANCH_NAME}" --json number --jq '.[].number')
|
||||||
|
if [ -z "$PR_EXISTS" ]; then
|
||||||
|
gh pr create --title "[Github Action] Update CHANGELOG.md" \
|
||||||
|
--body "This PR is auto-generated by a Github Action to update the CHANGELOG.md file." \
|
||||||
|
--head $BRANCH_NAME \
|
||||||
|
--base main \
|
||||||
|
--label "$AUTOMATED_PR_LABEL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Approve pull request
|
||||||
|
if: env.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(gh pr list --head "${BRANCH_NAME}" --json number --jq '.[].number')
|
||||||
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
|
gh pr review $PR_NUMBER --approve
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Re-approve pull request after update
|
||||||
|
if: env.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(gh pr list --head "${BRANCH_NAME}" --json number --jq '.[].number')
|
||||||
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
|
gh pr review $PR_NUMBER --approve
|
||||||
|
fi
|
122
.github/workflows/close-discussion.yml
vendored
Normal file
122
.github/workflows/close-discussion.yml
vendored
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
name: Close Discussion on PR Merge
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-discussion:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set Up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm install zx @octokit/graphql
|
||||||
|
|
||||||
|
- name: Close Discussion
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_BODY: ${{ github.event.pull_request.body }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
REPO_OWNER: ${{ github.repository_owner }}
|
||||||
|
REPO_NAME: ${{ github.event.repository.name }}
|
||||||
|
run: |
|
||||||
|
npx zx << 'EOF'
|
||||||
|
import { graphql } from "@octokit/graphql";
|
||||||
|
(async function() {
|
||||||
|
try {
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
const prBody = process.env.PR_BODY;
|
||||||
|
const prNumber = process.env.PR_NUMBER;
|
||||||
|
const owner = process.env.REPO_OWNER;
|
||||||
|
const repo = process.env.REPO_NAME;
|
||||||
|
|
||||||
|
if (!token || !prBody || !prNumber || !owner || !repo) {
|
||||||
|
console.log("Missing required environment variables.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = prBody.match(/#(\d+)/);
|
||||||
|
if (!match) {
|
||||||
|
console.log("No discussion ID found in PR body.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const discussionNumber = match[1];
|
||||||
|
|
||||||
|
console.log(`Extracted Discussion Number: ${discussionNumber}`);
|
||||||
|
console.log(`PR Number: ${prNumber}`);
|
||||||
|
console.log(`Repository: ${owner}/${repo}`);
|
||||||
|
|
||||||
|
const graphqlWithAuth = graphql.defaults({
|
||||||
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const discussionQuery = `
|
||||||
|
query($owner: String!, $repo: String!, $number: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
discussion(number: $number) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const discussionResponse = await graphqlWithAuth(discussionQuery, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
number: parseInt(discussionNumber, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
const discussionQLId = discussionResponse.repository.discussion.id;
|
||||||
|
if (!discussionQLId) {
|
||||||
|
console.log("Failed to fetch discussion GraphQL ID.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`GraphQL Discussion ID: ${discussionQLId}`);
|
||||||
|
|
||||||
|
const commentMutation = `
|
||||||
|
mutation($discussionId: ID!, $body: String!) {
|
||||||
|
addDiscussionComment(input: { discussionId: $discussionId, body: $body }) {
|
||||||
|
comment { id body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const commentResponse = await graphqlWithAuth(commentMutation, {
|
||||||
|
discussionId: discussionQLId,
|
||||||
|
body: `Merged with PR #${prNumber}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const commentId = commentResponse.addDiscussionComment.comment.id;
|
||||||
|
if (!commentId) {
|
||||||
|
console.log("Failed to post the comment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Comment Posted Successfully! Comment ID: ${commentId}`);
|
||||||
|
|
||||||
|
const markAnswerMutation = `
|
||||||
|
mutation($id: ID!) {
|
||||||
|
markDiscussionCommentAsAnswer(input: { id: $id }) {
|
||||||
|
discussion { id title }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
await graphqlWithAuth(markAnswerMutation, { id: commentId });
|
||||||
|
|
||||||
|
console.log("Comment marked as answer successfully!");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
EOF
|
37
.github/workflows/create-docker-for-runner.yml
vendored
Normal file
37
.github/workflows/create-docker-for-runner.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.github/runner/docker/**'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest #To ensure it always builds we use the github runner with all the right tooling
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
repo_name=${{ github.repository }} # Get repository name
|
||||||
|
repo_name_lower=$(echo $repo_name | tr '[:upper:]' '[:lower:]') # Convert to lowercase
|
||||||
|
docker build -t ghcr.io/$repo_name_lower/gh-runner-self:latest -f .github/runner/docker/gh-runner-self.dockerfile .
|
||||||
|
|
||||||
|
- name: Push Docker image to GHCR
|
||||||
|
run: |
|
||||||
|
repo_name=${{ github.repository }} # Get repository name
|
||||||
|
repo_name_lower=$(echo $repo_name | tr '[:upper:]' '[:lower:]') # Convert to lowercase
|
||||||
|
docker push ghcr.io/$repo_name_lower/gh-runner-self:latest
|
28
.github/workflows/delete-json-branch.yml
vendored
Normal file
28
.github/workflows/delete-json-branch.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
name: Delete JSON date PR Branch
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delete_branch:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
steps:
|
||||||
|
- name: Checkout the code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Delete PR Update Branch
|
||||||
|
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'pr-update-json-')
|
||||||
|
run: |
|
||||||
|
PR_BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
|
echo "Deleting branch $PR_BRANCH..."
|
||||||
|
|
||||||
|
# Avoid deleting the default branch (e.g., main)
|
||||||
|
if [[ "$PR_BRANCH" != "main" ]]; then
|
||||||
|
git push origin --delete "$PR_BRANCH"
|
||||||
|
else
|
||||||
|
echo "Skipping deletion of the main branch"
|
||||||
|
fi
|
78
.github/workflows/frontend-cicd.yml
vendored
Normal file
78
.github/workflows/frontend-cicd.yml
vendored
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Based on https://github.com/actions/starter-workflows/blob/main/pages/nextjs.yml
|
||||||
|
|
||||||
|
name: Frontend CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- frontend/**
|
||||||
|
- json/**
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
paths:
|
||||||
|
- frontend/**
|
||||||
|
- json/**
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend # Set default working directory for all run steps
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --prefer-offline --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test
|
||||||
|
|
||||||
|
- name: Configure Next.js for pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
with:
|
||||||
|
static_site_generator: next
|
||||||
|
|
||||||
|
- name: Build with Next.js
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: frontend/out
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
57
.github/workflows/github-release.yml
vendored
Normal file
57
.github/workflows/github-release.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
name: Create Daily Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '1 0 * * *' # Runs daily at 00:01 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-daily-release:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract first 5000 characters from CHANGELOG.md
|
||||||
|
run: head -c 5000 CHANGELOG.md > changelog_cropped.md
|
||||||
|
|
||||||
|
- name: Debugging - Show extracted changelog
|
||||||
|
run: |
|
||||||
|
echo "=== CHANGELOG EXCERPT ==="
|
||||||
|
cat changelog_cropped.md
|
||||||
|
echo "========================="
|
||||||
|
|
||||||
|
- name: Parse CHANGELOG.md and create release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
YESTERDAY=$(date -u --date="yesterday" +%Y-%m-%d)
|
||||||
|
echo "Checking for changes on: $YESTERDAY"
|
||||||
|
|
||||||
|
# Ensure yesterday's date exists in the changelog
|
||||||
|
if ! grep -q "## $YESTERDAY" changelog_cropped.md; then
|
||||||
|
echo "No entry found for $YESTERDAY, skipping release."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract section for yesterday's date
|
||||||
|
awk -v date="## $YESTERDAY" '
|
||||||
|
$0 ~ date {found=1; next}
|
||||||
|
found && /^## [0-9]{4}-[0-9]{2}-[0-9]{2}/ {exit}
|
||||||
|
found
|
||||||
|
' changelog_cropped.md > changelog_tmp.md
|
||||||
|
|
||||||
|
echo "=== Extracted Changelog ==="
|
||||||
|
cat changelog_tmp.md
|
||||||
|
echo "==========================="
|
||||||
|
|
||||||
|
# Skip if no content was found
|
||||||
|
if [ ! -s changelog_tmp.md ]; then
|
||||||
|
echo "No changes found for $YESTERDAY, skipping release."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create GitHub release
|
||||||
|
gh release create "$YESTERDAY" -t "$YESTERDAY" -F changelog_tmp.md
|
177
.github/workflows/script-test.yml
vendored
Normal file
177
.github/workflows/script-test.yml
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
name: Run Scripts on PVE Node for testing
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'install/**.sh'
|
||||||
|
- 'ct/**.sh'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-install-script:
|
||||||
|
runs-on: pvenode
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Add Git safe directory
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory /__w/ProxmoxVED/ProxmoxVE
|
||||||
|
|
||||||
|
- name: Set up GH_TOKEN
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "GH_TOKEN=${GH_TOKEN}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Get Changed Files
|
||||||
|
run: |
|
||||||
|
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
|
||||||
|
CHANGED_FILES=$(echo "$CHANGED_FILES" | tr '\n' ' ')
|
||||||
|
echo "Changed files: $CHANGED_FILES"
|
||||||
|
echo "SCRIPT=$CHANGED_FILES" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get scripts
|
||||||
|
id: check-install-script
|
||||||
|
run: |
|
||||||
|
ALL_FILES=()
|
||||||
|
ADDED_FILES=()
|
||||||
|
for FILE in ${{ env.SCRIPT }}; do
|
||||||
|
if [[ $FILE =~ ^install/.*-install\.sh$ ]] || [[ $FILE =~ ^ct/.*\.sh$ ]]; then
|
||||||
|
STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
|
||||||
|
if [[ ! " ${ADDED_FILES[@]} " =~ " $STRIPPED_NAME " ]]; then
|
||||||
|
ALL_FILES+=("$FILE")
|
||||||
|
ADDED_FILES+=("$STRIPPED_NAME") # Mark this base file as added (without the path)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
ALL_FILES=$(echo "${ALL_FILES[@]}" | xargs)
|
||||||
|
echo "$ALL_FILES"
|
||||||
|
echo "ALL_FILES=$ALL_FILES" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Run scripts
|
||||||
|
id: run-install
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
#run for each files in /ct
|
||||||
|
for FILE in ${{ env.ALL_FILES }}; do
|
||||||
|
STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
|
||||||
|
echo "Running Test for: $STRIPPED_NAME"
|
||||||
|
if grep -E -q 'read\s+-r\s+-p\s+".*"\s+\w+' "$FILE"; then
|
||||||
|
echo "The script contains an interactive prompt. Skipping execution."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ $FILE =~ ^install/.*-install\.sh$ ]]; then
|
||||||
|
CT_SCRIPT="ct/$STRIPPED_NAME.sh"
|
||||||
|
if [[ ! -f $CT_SCRIPT ]]; then
|
||||||
|
echo "No CT script found for $STRIPPED_NAME"
|
||||||
|
ERROR_MSG="No CT script found for $FILE"
|
||||||
|
echo "$ERROR_MSG" > result_$STRIPPED_NAME.log
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if grep -E -q 'read\s+-r\s+-p\s+".*"\s+\w+' "install/$STRIPPED_NAME-install.sh"; then
|
||||||
|
echo "The script contains an interactive prompt. Skipping execution."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "Found CT script for $STRIPPED_NAME"
|
||||||
|
chmod +x "$CT_SCRIPT"
|
||||||
|
RUNNING_FILE=$CT_SCRIPT
|
||||||
|
elif [[ $FILE =~ ^ct/.*\.sh$ ]]; then
|
||||||
|
INSTALL_SCRIPT="install/$STRIPPED_NAME-install.sh"
|
||||||
|
if [[ ! -f $INSTALL_SCRIPT ]]; then
|
||||||
|
echo "No install script found for $STRIPPED_NAME"
|
||||||
|
ERROR_MSG="No install script found for $FILE"
|
||||||
|
echo "$ERROR_MSG" > result_$STRIPPED_NAME.log
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "Found install script for $STRIPPED_NAME"
|
||||||
|
chmod +x "$INSTALL_SCRIPT"
|
||||||
|
RUNNING_FILE=$FILE
|
||||||
|
if grep -E -q 'read\s+-r\s+-p\s+".*"\s+\w+' "ct/$STRIPPED_NAME.sh"; then
|
||||||
|
echo "The script contains an interactive prompt. Skipping execution."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
git remote add community-scripts https://github.com/community-scripts/ProxmoxVE.git
|
||||||
|
git fetch community-scripts
|
||||||
|
rm -f .github/workflows/scripts/app-test/pr-build.func || true
|
||||||
|
rm -f .github/workflows/scripts/app-test/pr-install.func || true
|
||||||
|
rm -f .github/workflows/scripts/app-test/pr-alpine-install.func || true
|
||||||
|
rm -f .github/workflows/scripts/app-test/pr-create-lxc.sh || true
|
||||||
|
git checkout community-scripts/main -- .github/workflows/scripts/app-test/pr-build.func
|
||||||
|
git checkout community-scripts/main -- .github/workflows/scripts/app-test/pr-install.func
|
||||||
|
git checkout community-scripts/main -- .github/workflows/scripts/app-test/pr-alpine-install.func
|
||||||
|
git checkout community-scripts/main -- .github/workflows/scripts/app-test/pr-create-lxc.sh
|
||||||
|
chmod +x $RUNNING_FILE
|
||||||
|
chmod +x .github/workflows/scripts/app-test/pr-create-lxc.sh
|
||||||
|
chmod +x .github/workflows/scripts/app-test/pr-install.func
|
||||||
|
chmod +x .github/workflows/scripts/app-test/pr-alpine-install.func
|
||||||
|
chmod +x .github/workflows/scripts/app-test/pr-build.func
|
||||||
|
sed -i 's|source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)|source .github/workflows/scripts/app-test/pr-build.func|g' "$RUNNING_FILE"
|
||||||
|
echo "Executing $RUNNING_FILE"
|
||||||
|
ERROR_MSG=$(./$RUNNING_FILE 2>&1 > /dev/null)
|
||||||
|
echo "Finished running $FILE"
|
||||||
|
if [ -n "$ERROR_MSG" ]; then
|
||||||
|
echo "ERROR in $STRIPPED_NAME: $ERROR_MSG"
|
||||||
|
echo "$ERROR_MSG" > result_$STRIPPED_NAME.log
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -e # Restore exit-on-error
|
||||||
|
|
||||||
|
- name: Cleanup PVE Node
|
||||||
|
run: |
|
||||||
|
containers=$(pct list | tail -n +2 | awk '{print $0 " " $4}' | awk '{print $1}')
|
||||||
|
|
||||||
|
for container_id in $containers; do
|
||||||
|
status=$(pct status $container_id | awk '{print $2}')
|
||||||
|
if [[ $status == "running" ]]; then
|
||||||
|
pct stop $container_id
|
||||||
|
pct destroy $container_id
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Post error comments
|
||||||
|
run: |
|
||||||
|
ERROR="false"
|
||||||
|
SEARCH_LINE=".github/workflows/scripts/app-test/pr-build.func: line 255:"
|
||||||
|
|
||||||
|
# Get all existing comments on the PR
|
||||||
|
EXISTING_COMMENTS=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json comments --jq '.comments[].body')
|
||||||
|
|
||||||
|
for FILE in ${{ env.ALL_FILES }}; do
|
||||||
|
STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
|
||||||
|
if [[ ! -f result_$STRIPPED_NAME.log ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
ERROR_MSG=$(cat result_$STRIPPED_NAME.log)
|
||||||
|
|
||||||
|
if [ -n "$ERROR_MSG" ]; then
|
||||||
|
CLEANED_ERROR_MSG=$(echo "$ERROR_MSG" | sed "s|$SEARCH_LINE.*||")
|
||||||
|
COMMENT_BODY=":warning: The script _**$FILE**_ failed with the following message: <br> <div><strong>${CLEANED_ERROR_MSG}</strong></div>"
|
||||||
|
|
||||||
|
# Check if the comment already exists
|
||||||
|
if echo "$EXISTING_COMMENTS" | grep -qF "$COMMENT_BODY"; then
|
||||||
|
echo "Skipping duplicate comment for $FILE"
|
||||||
|
else
|
||||||
|
echo "Posting error message for $FILE"
|
||||||
|
gh pr comment ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--body "$COMMENT_BODY"
|
||||||
|
ERROR="true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "ERROR=$ERROR" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
|
243
.github/workflows/script_format.yml
vendored
Normal file
243
.github/workflows/script_format.yml
vendored
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
name: Script Format Check
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'install/*.sh'
|
||||||
|
- 'ct/*.sh'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-install-script:
|
||||||
|
runs-on: pvenode
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR branch (supports forks)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Add Git safe directory
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory /__w/ProxmoxVED/ProxmoxVE
|
||||||
|
|
||||||
|
- name: Set up GH_TOKEN
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "GH_TOKEN=${GH_TOKEN}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Get Changed Files
|
||||||
|
run: |
|
||||||
|
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
|
||||||
|
CHANGED_FILES=$(echo "$CHANGED_FILES" | tr '\n' ' ')
|
||||||
|
echo "Changed files: $CHANGED_FILES"
|
||||||
|
echo "SCRIPT=$CHANGED_FILES" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Check scripts
|
||||||
|
id: run-install
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
for FILE in ${{ env.SCRIPT }}; do
|
||||||
|
STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
|
||||||
|
echo "Running Test for: $STRIPPED_NAME"
|
||||||
|
FILE_STRIPPED="${FILE##*/}"
|
||||||
|
LOG_FILE="result_$FILE_STRIPPED.log"
|
||||||
|
|
||||||
|
if [[ $FILE =~ ^ct/.*\.sh$ ]]; then
|
||||||
|
|
||||||
|
FIRST_LINE=$(sed -n '1p' "$FILE")
|
||||||
|
[[ "$FIRST_LINE" != "#!/usr/bin/env bash" ]] && echo "Line 1 was $FIRST_LINE | Should be: #!/usr/bin/env bash" >> "$LOG_FILE"
|
||||||
|
SECOND_LINE=$(sed -n '2p' "$FILE")
|
||||||
|
[[ "$SECOND_LINE" != "source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)" ]] &&
|
||||||
|
echo "Line 2 was $SECOND_LINE | Should be: source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)" >> "$LOG_FILE"
|
||||||
|
THIRD_LINE=$(sed -n '3p' "$FILE")
|
||||||
|
if ! [[ "$THIRD_LINE" =~ ^#\ Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ community-scripts\ ORG$ || "$THIRD_LINE" =~ ^Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ tteck$ ]]; then
|
||||||
|
echo "Line 3 was $THIRD_LINE | Should be: # Copyright (c) 2021-2025 community-scripts ORG" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPECTED_AUTHOR="# Author:"
|
||||||
|
EXPECTED_LICENSE="# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE"
|
||||||
|
EXPECTED_SOURCE="# Source:"
|
||||||
|
EXPECTED_EMPTY=""
|
||||||
|
|
||||||
|
for i in {4..7}; do
|
||||||
|
LINE=$(sed -n "${i}p" "$FILE")
|
||||||
|
|
||||||
|
case $i in
|
||||||
|
4)
|
||||||
|
[[ $LINE == $EXPECTED_AUTHOR* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_AUTHOR" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
[[ "$LINE" == "$EXPECTED_LICENSE" ]] || printf "Line %d was: '%s' | Should be: '%s'\n" "$i" "$LINE" "$EXPECTED_LICENSE" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
[[ $LINE == $EXPECTED_SOURCE* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_SOURCE" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
[[ -z $LINE ]] || printf "Line %d was: '%s' | Should be empty\n" "$i" "$LINE" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_PREFIXES=(
|
||||||
|
"APP="
|
||||||
|
"var_tags="
|
||||||
|
"var_cpu=" # Must be a number
|
||||||
|
"var_ram=" # Must be a number
|
||||||
|
"var_disk=" # Must be a number
|
||||||
|
"var_os=" # Must be debian, alpine, or ubuntu
|
||||||
|
"var_version="
|
||||||
|
"var_unprivileged=" # Must be 0 or 1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
for i in {8..15}; do
|
||||||
|
LINE=$(sed -n "${i}p" "$FILE")
|
||||||
|
INDEX=$((i - 8))
|
||||||
|
|
||||||
|
case $INDEX in
|
||||||
|
2|3|4) # var_cpu, var_ram, var_disk (must be numbers)
|
||||||
|
if [[ "$LINE" =~ ^${EXPECTED_PREFIXES[$INDEX]}([0-9]+)$ ]]; then
|
||||||
|
continue # Valid
|
||||||
|
else
|
||||||
|
echo "Line $i was '$LINE' | Should be: '${EXPECTED_PREFIXES[$INDEX]}<NUMBER>'" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
5) # var_os (must be debian, alpine, or ubuntu)
|
||||||
|
if [[ "$LINE" =~ ^var_os=(debian|alpine|ubuntu)$ ]]; then
|
||||||
|
continue # Valid
|
||||||
|
else
|
||||||
|
echo "Line $i was '$LINE' | Should be: 'var_os=[debian|alpine|ubuntu]'" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
7) # var_unprivileged (must be 0 or 1)
|
||||||
|
if [[ "$LINE" =~ ^var_unprivileged=[01]$ ]]; then
|
||||||
|
continue # Valid
|
||||||
|
else
|
||||||
|
echo "Line $i was '$LINE' | Should be: 'var_unprivileged=[0|1]'" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*) # Other lines (must start with expected prefix)
|
||||||
|
if [[ "$LINE" == ${EXPECTED_PREFIXES[$INDEX]}* ]]; then
|
||||||
|
continue # Valid
|
||||||
|
else
|
||||||
|
echo "Line $i was '$LINE' | Should start with '${EXPECTED_PREFIXES[$INDEX]}'" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
for i in {16..20}; do
|
||||||
|
LINE=$(sed -n "${i}p" "$FILE")
|
||||||
|
EXPECTED=(
|
||||||
|
"header_info \"$APP\""
|
||||||
|
"variables"
|
||||||
|
"color"
|
||||||
|
"catch_errors"
|
||||||
|
"function update_script() {"
|
||||||
|
)
|
||||||
|
[[ "$LINE" != "${EXPECTED[$((i-16))]}" ]] && echo "Line $i was $LINE | Should be: ${EXPECTED[$((i-16))]}" >> "$LOG_FILE"
|
||||||
|
done
|
||||||
|
cat "$LOG_FILE"
|
||||||
|
elif [[ $FILE =~ ^install/.*-install\.sh$ ]]; then
|
||||||
|
|
||||||
|
FIRST_LINE=$(sed -n '1p' "$FILE")
|
||||||
|
[[ "$FIRST_LINE" != "#!/usr/bin/env bash" ]] && echo "Line 1 was $FIRST_LINE | Should be: #!/usr/bin/env bash" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
SECOND_LINE=$(sed -n '2p' "$FILE")
|
||||||
|
[[ -n "$SECOND_LINE" ]] && echo "Line 2 should be empty" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
THIRD_LINE=$(sed -n '3p' "$FILE")
|
||||||
|
if ! [[ "$THIRD_LINE" =~ ^#\ Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ community-scripts\ ORG$ || "$THIRD_LINE" =~ ^Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ tteck$ ]]; then
|
||||||
|
echo "Line 3 was $THIRD_LINE | Should be: # Copyright (c) 2021-2025 community-scripts ORG" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPECTED_AUTHOR="# Author:"
|
||||||
|
EXPECTED_LICENSE="# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE"
|
||||||
|
EXPECTED_SOURCE="# Source:"
|
||||||
|
EXPECTED_EMPTY=""
|
||||||
|
|
||||||
|
for i in {4..7}; do
|
||||||
|
LINE=$(sed -n "${i}p" "$FILE")
|
||||||
|
|
||||||
|
case $i in
|
||||||
|
4)
|
||||||
|
[[ $LINE == $EXPECTED_AUTHOR* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_AUTHOR" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
[[ "$LINE" == "$EXPECTED_LICENSE" ]] || printf "Line %d was: '%s' | Should be: '%s'\n" "$i" "$LINE" "$EXPECTED_LICENSE" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
[[ $LINE == $EXPECTED_SOURCE* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_SOURCE" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
[[ -z $LINE ]] || printf "Line %d was: '%s' | Should be empty\n" "$i" "$LINE" >> $LOG_FILE
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$(sed -n '8p' "$FILE")" != 'source /dev/stdin <<< "$FUNCTIONS_FILE_PATH"' ]] && echo 'Line 8 should be: source /dev/stdin <<< "$FUNCTIONS_FILE_PATH"' >> "$LOG_FILE"
|
||||||
|
|
||||||
|
for i in {9..14}; do
|
||||||
|
LINE=$(sed -n "${i}p" "$FILE")
|
||||||
|
EXPECTED=(
|
||||||
|
"color"
|
||||||
|
"verb_ip6"
|
||||||
|
"catch_errors"
|
||||||
|
"setting_up_container"
|
||||||
|
"network_check"
|
||||||
|
"update_os"
|
||||||
|
)
|
||||||
|
[[ "$LINE" != "${EXPECTED[$((i-9))]}" ]] && echo "Line $i was $LINE | Should be: ${EXPECTED[$((i-9))]}" >> "$LOG_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$(sed -n '15p' "$FILE")" ]] && echo "Line 15 should be empty" >> "$LOG_FILE"
|
||||||
|
[[ "$(sed -n '16p' "$FILE")" != 'msg_info "Installing Dependencies"' ]] && echo 'Line 16 should be: msg_info "Installing Dependencies"' >> "$LOG_FILE"
|
||||||
|
|
||||||
|
LAST_3_LINES=$(tail -n 3 "$FILE")
|
||||||
|
[[ "$LAST_3_LINES" != *"$STD apt-get -y autoremove"* ]] && echo 'Third to last line should be: $STD apt-get -y autoremove' >> "$LOG_FILE"
|
||||||
|
[[ "$LAST_3_LINES" != *"$STD apt-get -y autoclean"* ]] && echo 'Second to last line should be: $STD apt-get -y clean' >> "$LOG_FILE"
|
||||||
|
[[ "$LAST_3_LINES" != *'msg_ok "Cleaned"'* ]] && echo 'Last line should be: msg_ok "Cleaned"' >> "$LOG_FILE"
|
||||||
|
cat "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
- name: Post error comments
|
||||||
|
run: |
|
||||||
|
ERROR="false"
|
||||||
|
for FILE in ${{ env.SCRIPT }}; do
|
||||||
|
FILE_STRIPPED="${FILE##*/}"
|
||||||
|
LOG_FILE="result_$FILE_STRIPPED.log"
|
||||||
|
echo $LOG_FILE
|
||||||
|
if [[ ! -f $LOG_FILE ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
ERROR_MSG=$(cat $LOG_FILE)
|
||||||
|
|
||||||
|
if [ -n "$ERROR_MSG" ]; then
|
||||||
|
echo "Posting error message for $FILE"
|
||||||
|
echo ${ERROR_MSG}
|
||||||
|
gh pr comment ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--body ":warning: The script _**$FILE**_ has the following formatting errors: <br> <div><strong>${ERROR_MSG}</strong></div>"
|
||||||
|
|
||||||
|
|
||||||
|
ERROR="true"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "ERROR=$ERROR" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Fail if error
|
||||||
|
if: ${{ env.ERROR == 'true' }}
|
||||||
|
run: exit 1
|
86
.github/workflows/scripts/app-test/pr-alpine-install.func
vendored
Normal file
86
.github/workflows/scripts/app-test/pr-alpine-install.func
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: Michel Roegl-Brunner (michelroegl-brunner)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
|
||||||
|
color() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch_errors() {
|
||||||
|
set -Eeuo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function handles errors
|
||||||
|
error_handler() {
|
||||||
|
local line_number="$1"
|
||||||
|
local command="$2"
|
||||||
|
SCRIPT_NAME=$(basename "$0")
|
||||||
|
local error_message="$SCRIPT_NAME: Failure in line $line_number while executing command $command"
|
||||||
|
echo -e "\n$error_message"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
verb_ip6() {
|
||||||
|
STD=""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_info() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -ne "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_ok() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_error() {
|
||||||
|
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRY_NUM=10
|
||||||
|
RETRY_EVERY=3
|
||||||
|
i=$RETRY_NUM
|
||||||
|
|
||||||
|
setting_up_container() {
|
||||||
|
while [ $i -gt 0 ]; do
|
||||||
|
if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" != "" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo 1>&2 -en "No Network! "
|
||||||
|
sleep $RETRY_EVERY
|
||||||
|
i=$((i - 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then
|
||||||
|
echo 1>&2 -e "\n No Network After $RETRY_NUM Tries"
|
||||||
|
echo -e "Check Network Settings"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
msg_ok "Set up Container OS"
|
||||||
|
msg_ok "Network Connected: $(hostname -i)"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_check() {
|
||||||
|
RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
|
||||||
|
if [[ -z "$RESOLVEDIP" ]]; then msg_error "DNS Lookup Failure"; else msg_ok "DNS Resolved github.com to $RESOLVEDIP"; fi
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
update_os() {
|
||||||
|
msg_info "Updating Container OS"
|
||||||
|
apk update
|
||||||
|
apk upgrade
|
||||||
|
msg_ok "Updated Container OS"
|
||||||
|
}
|
||||||
|
|
||||||
|
motd_ssh() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customize() {
|
||||||
|
return
|
||||||
|
}
|
260
.github/workflows/scripts/app-test/pr-build.func
vendored
Normal file
260
.github/workflows/scripts/app-test/pr-build.func
vendored
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: Michel Roegl-Brunner (michelroegl-brunner)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
|
||||||
|
variables() {
|
||||||
|
NSAPP=$(echo ${APP,,} | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
|
||||||
|
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NEXTID=$(pvesh get /cluster/nextid)
|
||||||
|
timezone=$(cat /etc/timezone)
|
||||||
|
header_info() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
base_settings() {
|
||||||
|
# Default Settings
|
||||||
|
CT_TYPE="1"
|
||||||
|
DISK_SIZE="4"
|
||||||
|
CORE_COUNT="1"
|
||||||
|
RAM_SIZE="1024"
|
||||||
|
VERBOSE="no"
|
||||||
|
PW=""
|
||||||
|
CT_ID=$NEXTID
|
||||||
|
HN=$NSAPP
|
||||||
|
BRG="vmbr0"
|
||||||
|
NET="dhcp"
|
||||||
|
GATE=""
|
||||||
|
APT_CACHER=""
|
||||||
|
APT_CACHER_IP=""
|
||||||
|
DISABLEIP6="no"
|
||||||
|
MTU=""
|
||||||
|
SD=""
|
||||||
|
NS=""
|
||||||
|
MAC=""
|
||||||
|
VLAN=""
|
||||||
|
SSH="no"
|
||||||
|
SSH_AUTHORIZED_KEY=""
|
||||||
|
TAGS="community-script;"
|
||||||
|
|
||||||
|
# Override default settings with variables from ct script
|
||||||
|
CT_TYPE=${var_unprivileged:-$CT_TYPE}
|
||||||
|
DISK_SIZE=${var_disk:-$DISK_SIZE}
|
||||||
|
CORE_COUNT=${var_cpu:-$CORE_COUNT}
|
||||||
|
RAM_SIZE=${var_ram:-$RAM_SIZE}
|
||||||
|
VERB=${var_verbose:-$VERBOSE}
|
||||||
|
TAGS="${TAGS}${var_tags:-}"
|
||||||
|
|
||||||
|
# Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
|
||||||
|
if [ -z "$var_os" ]; then
|
||||||
|
var_os="debian"
|
||||||
|
fi
|
||||||
|
if [ -z "$var_version" ]; then
|
||||||
|
var_version="12"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
color() {
|
||||||
|
# Colors
|
||||||
|
YW=$(echo "\033[33m")
|
||||||
|
YWB=$(echo "\033[93m")
|
||||||
|
BL=$(echo "\033[36m")
|
||||||
|
RD=$(echo "\033[01;31m")
|
||||||
|
BGN=$(echo "\033[4;92m")
|
||||||
|
GN=$(echo "\033[1;92m")
|
||||||
|
DGN=$(echo "\033[32m")
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
CL=$(echo "\033[m")
|
||||||
|
UL=$(echo "\033[4m")
|
||||||
|
BOLD=$(echo "\033[1m")
|
||||||
|
BFR="\\r\\033[K"
|
||||||
|
HOLD=" "
|
||||||
|
TAB=" "
|
||||||
|
|
||||||
|
# Icons
|
||||||
|
CM="${TAB}✔️${TAB}${CL}"
|
||||||
|
CROSS="${TAB}✖️${TAB}${CL}"
|
||||||
|
INFO="${TAB}💡${TAB}${CL}"
|
||||||
|
OS="${TAB}🖥️${TAB}${CL}"
|
||||||
|
OSVERSION="${TAB}🌟${TAB}${CL}"
|
||||||
|
CONTAINERTYPE="${TAB}📦${TAB}${CL}"
|
||||||
|
DISKSIZE="${TAB}💾${TAB}${CL}"
|
||||||
|
CPUCORE="${TAB}🧠${TAB}${CL}"
|
||||||
|
RAMSIZE="${TAB}🛠️${TAB}${CL}"
|
||||||
|
SEARCH="${TAB}🔍${TAB}${CL}"
|
||||||
|
VERIFYPW="${TAB}🔐${TAB}${CL}"
|
||||||
|
CONTAINERID="${TAB}🆔${TAB}${CL}"
|
||||||
|
HOSTNAME="${TAB}🏠${TAB}${CL}"
|
||||||
|
BRIDGE="${TAB}🌉${TAB}${CL}"
|
||||||
|
NETWORK="${TAB}📡${TAB}${CL}"
|
||||||
|
GATEWAY="${TAB}🌐${TAB}${CL}"
|
||||||
|
DISABLEIPV6="${TAB}🚫${TAB}${CL}"
|
||||||
|
DEFAULT="${TAB}⚙️${TAB}${CL}"
|
||||||
|
MACADDRESS="${TAB}🔗${TAB}${CL}"
|
||||||
|
VLANTAG="${TAB}🏷️${TAB}${CL}"
|
||||||
|
ROOTSSH="${TAB}🔑${TAB}${CL}"
|
||||||
|
CREATING="${TAB}🚀${TAB}${CL}"
|
||||||
|
ADVANCED="${TAB}🧩${TAB}${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
catch_errors() {
|
||||||
|
set -Eeuo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function handles errors
|
||||||
|
error_handler() {
|
||||||
|
local line_number="$1"
|
||||||
|
local command="$2"
|
||||||
|
SCRIPT_NAME=$(basename "$0")
|
||||||
|
local error_message="$SCRIPT_NAME: Failure in line $line_number while executing command $command"
|
||||||
|
echo -e "\n$error_message"
|
||||||
|
exit 100
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_info() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -ne "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_ok() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_error() {
|
||||||
|
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
base_settings
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
build_container() {
|
||||||
|
# if [ "$VERB" == "yes" ]; then set -x; fi
|
||||||
|
|
||||||
|
if [ "$CT_TYPE" == "1" ]; then
|
||||||
|
FEATURES="keyctl=1,nesting=1"
|
||||||
|
else
|
||||||
|
FEATURES="nesting=1"
|
||||||
|
fi
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
pushd $TEMP_DIR >/dev/null
|
||||||
|
if [ "$var_os" == "alpine" ]; then
|
||||||
|
export FUNCTIONS_FILE_PATH="$(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/.github/workflows/scripts/app-test/pr-alpine-install.func)"
|
||||||
|
else
|
||||||
|
export FUNCTIONS_FILE_PATH="$(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/.github/workflows/scripts/app-test/pr-install.func)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export CACHER="$APT_CACHER"
|
||||||
|
export CACHER_IP="$APT_CACHER_IP"
|
||||||
|
export tz=""
|
||||||
|
export DISABLEIPV6="$DISABLEIP6"
|
||||||
|
export APPLICATION="$APP"
|
||||||
|
export app="$NSAPP"
|
||||||
|
export PASSWORD="$PW"
|
||||||
|
export VERBOSE="$VERB"
|
||||||
|
export SSH_ROOT="${SSH}"
|
||||||
|
export SSH_AUTHORIZED_KEY
|
||||||
|
export CTID="$CT_ID"
|
||||||
|
export CTTYPE="$CT_TYPE"
|
||||||
|
export PCT_OSTYPE="$var_os"
|
||||||
|
export PCT_OSVERSION="$var_version"
|
||||||
|
export PCT_DISK_SIZE="$DISK_SIZE"
|
||||||
|
export tz="$timezone"
|
||||||
|
export PCT_OPTIONS="
|
||||||
|
-features $FEATURES
|
||||||
|
-hostname $HN
|
||||||
|
-tags $TAGS
|
||||||
|
$SD
|
||||||
|
$NS
|
||||||
|
-net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU
|
||||||
|
-onboot 1
|
||||||
|
-cores $CORE_COUNT
|
||||||
|
-memory $RAM_SIZE
|
||||||
|
-unprivileged $CT_TYPE
|
||||||
|
$PW
|
||||||
|
"
|
||||||
|
echo "Container ID: $CTID"
|
||||||
|
|
||||||
|
# This executes create_lxc.sh and creates the container and .conf file
|
||||||
|
bash -c "$(wget -qLO - https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/.github/workflows/scripts/app-test/pr-create-lxc.sh)"
|
||||||
|
|
||||||
|
LXC_CONFIG=/etc/pve/lxc/${CTID}.conf
|
||||||
|
if [ "$CT_TYPE" == "0" ]; then
|
||||||
|
cat <<EOF >>$LXC_CONFIG
|
||||||
|
# USB passthrough
|
||||||
|
lxc.cgroup2.devices.allow: a
|
||||||
|
lxc.cap.drop:
|
||||||
|
lxc.cgroup2.devices.allow: c 188:* rwm
|
||||||
|
lxc.cgroup2.devices.allow: c 189:* rwm
|
||||||
|
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
|
||||||
|
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
|
||||||
|
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
|
||||||
|
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
|
||||||
|
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CT_TYPE" == "0" ]; then
|
||||||
|
if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" || "$APP" == "Ollama" ]]; then
|
||||||
|
cat <<EOF >>$LXC_CONFIG
|
||||||
|
# VAAPI hardware transcoding
|
||||||
|
lxc.cgroup2.devices.allow: c 226:0 rwm
|
||||||
|
lxc.cgroup2.devices.allow: c 226:128 rwm
|
||||||
|
lxc.cgroup2.devices.allow: c 29:0 rwm
|
||||||
|
lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file
|
||||||
|
lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir
|
||||||
|
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" || "$APP" == "Ollama" ]]; then
|
||||||
|
if [[ -e "/dev/dri/renderD128" ]]; then
|
||||||
|
if [[ -e "/dev/dri/card0" ]]; then
|
||||||
|
cat <<EOF >>$LXC_CONFIG
|
||||||
|
# VAAPI hardware transcoding
|
||||||
|
dev0: /dev/dri/card0,gid=44
|
||||||
|
dev1: /dev/dri/renderD128,gid=104
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
cat <<EOF >>$LXC_CONFIG
|
||||||
|
# VAAPI hardware transcoding
|
||||||
|
dev0: /dev/dri/card1,gid=44
|
||||||
|
dev1: /dev/dri/renderD128,gid=104
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This starts the container and executes <app>-install.sh
|
||||||
|
msg_info "Starting LXC Container"
|
||||||
|
pct start "$CTID"
|
||||||
|
msg_ok "Started LXC Container"
|
||||||
|
|
||||||
|
if [[ ! -f "/root/actions-runner/_work/ProxmoxVED/ProxmoxVED/install/$var_install.sh" ]]; then
|
||||||
|
msg_error "No install script found for $APP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$var_os" == "alpine" ]; then
|
||||||
|
sleep 3
|
||||||
|
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
|
||||||
|
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
|
||||||
|
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
|
||||||
|
EOF'
|
||||||
|
pct exec "$CTID" -- ash -c "apk add bash >/dev/null"
|
||||||
|
fi
|
||||||
|
lxc-attach -n "$CTID" -- bash -c "$(cat /root/actions-runner/_work/ProxmoxVED/ProxmoxVED/install/$var_install.sh)"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
description() {
|
||||||
|
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
|
||||||
|
}
|
163
.github/workflows/scripts/app-test/pr-create-lxc.sh
vendored
Normal file
163
.github/workflows/scripts/app-test/pr-create-lxc.sh
vendored
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: Michel Roegl-Brunner (michelroegl-brunner)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
|
||||||
|
color() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch_errors() {
|
||||||
|
set -Eeuo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function handles errors
|
||||||
|
error_handler() {
|
||||||
|
local exit_code="$?"
|
||||||
|
local line_number="$1"
|
||||||
|
local command="$2"
|
||||||
|
local error_message="Failure in line $line_number: exit code $exit_code: while executing command $command"
|
||||||
|
echo -e "\n$error_message"
|
||||||
|
exit 100
|
||||||
|
}
|
||||||
|
verb_ip6() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_info() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -ne "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_ok() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_error() {
|
||||||
|
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
VALIDCT=$(pvesm status -content rootdir | awk 'NR>1')
|
||||||
|
if [ -z "$VALIDCT" ]; then
|
||||||
|
msg_error "Unable to detect a valid Container Storage location."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VALIDTMP=$(pvesm status -content vztmpl | awk 'NR>1')
|
||||||
|
if [ -z "$VALIDTMP" ]; then
|
||||||
|
msg_error "Unable to detect a valid Template Storage location."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
function select_storage() {
|
||||||
|
local CLASS=$1
|
||||||
|
local CONTENT
|
||||||
|
local CONTENT_LABEL
|
||||||
|
case $CLASS in
|
||||||
|
container)
|
||||||
|
CONTENT='rootdir'
|
||||||
|
CONTENT_LABEL='Container'
|
||||||
|
;;
|
||||||
|
template)
|
||||||
|
CONTENT='vztmpl'
|
||||||
|
CONTENT_LABEL='Container template'
|
||||||
|
;;
|
||||||
|
*) false || {
|
||||||
|
msg_error "Invalid storage class."
|
||||||
|
exit 201
|
||||||
|
} ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# This Queries all storage locations
|
||||||
|
local -a MENU
|
||||||
|
while read -r line; do
|
||||||
|
local TAG=$(echo $line | awk '{print $1}')
|
||||||
|
local TYPE=$(echo $line | awk '{printf "%-10s", $2}')
|
||||||
|
local FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}')
|
||||||
|
local ITEM="Type: $TYPE Free: $FREE "
|
||||||
|
local OFFSET=2
|
||||||
|
if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
|
||||||
|
local MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
|
||||||
|
fi
|
||||||
|
MENU+=("$TAG" "$ITEM" "OFF")
|
||||||
|
done < <(pvesm status -content $CONTENT | awk 'NR>1')
|
||||||
|
|
||||||
|
# Select storage location
|
||||||
|
if [ $((${#MENU[@]} / 3)) -eq 1 ]; then
|
||||||
|
printf ${MENU[0]}
|
||||||
|
else
|
||||||
|
msg_error "STORAGE ISSUES!"
|
||||||
|
exit 202
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "${CTID:-}" ]] || {
|
||||||
|
msg_error "You need to set 'CTID' variable."
|
||||||
|
exit 203
|
||||||
|
}
|
||||||
|
[[ "${PCT_OSTYPE:-}" ]] || {
|
||||||
|
msg_error "You need to set 'PCT_OSTYPE' variable."
|
||||||
|
exit 204
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test if ID is valid
|
||||||
|
[ "$CTID" -ge "100" ] || {
|
||||||
|
msg_error "ID cannot be less than 100."
|
||||||
|
exit 205
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test if ID is in use
|
||||||
|
if pct status $CTID &>/dev/null; then
|
||||||
|
echo -e "ID '$CTID' is already in use."
|
||||||
|
unset CTID
|
||||||
|
msg_error "Cannot use ID that is already in use."
|
||||||
|
exit 206
|
||||||
|
fi
|
||||||
|
|
||||||
|
TEMPLATE_STORAGE=$(select_storage template) || exit
|
||||||
|
|
||||||
|
CONTAINER_STORAGE=$(select_storage container) || exit
|
||||||
|
|
||||||
|
pveam update >/dev/null
|
||||||
|
|
||||||
|
TEMPLATE_SEARCH=${PCT_OSTYPE}-${PCT_OSVERSION:-}
|
||||||
|
mapfile -t TEMPLATES < <(pveam available -section system | sed -n "s/.*\($TEMPLATE_SEARCH.*\)/\1/p" | sort -t - -k 2 -V)
|
||||||
|
[ ${#TEMPLATES[@]} -gt 0 ] || {
|
||||||
|
msg_error "Unable to find a template when searching for '$TEMPLATE_SEARCH'."
|
||||||
|
exit 207
|
||||||
|
}
|
||||||
|
TEMPLATE="${TEMPLATES[-1]}"
|
||||||
|
|
||||||
|
TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
|
||||||
|
|
||||||
|
if ! pveam list "$TEMPLATE_STORAGE" | grep -q "$TEMPLATE"; then
|
||||||
|
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
|
||||||
|
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null ||
|
||||||
|
{
|
||||||
|
msg_error "A problem occurred while downloading the LXC template."
|
||||||
|
exit 208
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
|
||||||
|
grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
|
||||||
|
|
||||||
|
PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
|
||||||
|
[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
|
||||||
|
|
||||||
|
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then
|
||||||
|
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
|
||||||
|
|
||||||
|
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null ||
|
||||||
|
{
|
||||||
|
msg_error "A problem occurred while re-downloading the LXC template."
|
||||||
|
exit 208
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then
|
||||||
|
msg_error "A problem occurred while trying to create container after re-downloading template."
|
||||||
|
exit 200
|
||||||
|
fi
|
||||||
|
fi
|
93
.github/workflows/scripts/app-test/pr-install.func
vendored
Normal file
93
.github/workflows/scripts/app-test/pr-install.func
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: Michel Roegl-Brunner (michelroegl-brunner)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
|
||||||
|
color() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
catch_errors() {
|
||||||
|
set -Euo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
error_handler() {
|
||||||
|
local line_number="$1"
|
||||||
|
local command="$2"
|
||||||
|
local error_message="Failure in line $line_number while executing command '$command'"
|
||||||
|
echo -e "\n$error_message\n" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
verb_ip6() {
|
||||||
|
STD="silent"
|
||||||
|
silent() {
|
||||||
|
"$@" >/dev/null 2>&1 || error_handler "${BASH_LINENO[0]}" "$*"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_info() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -ne "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_ok() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_error() {
|
||||||
|
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${msg}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRY_NUM=10
|
||||||
|
RETRY_EVERY=3
|
||||||
|
setting_up_container() {
|
||||||
|
|
||||||
|
sed -i "/$LANG/ s/\(^# \)//" /etc/locale.gen
|
||||||
|
locale_line=$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print $1}' | head -n 1)
|
||||||
|
echo "LANG=${locale_line}" >/etc/default/locale
|
||||||
|
locale-gen >/dev/null
|
||||||
|
export LANG=${locale_line}
|
||||||
|
echo $tz >/etc/timezone
|
||||||
|
ln -sf /usr/share/zoneinfo/$tz /etc/localtime
|
||||||
|
|
||||||
|
for ((i = RETRY_NUM; i > 0; i--)); do
|
||||||
|
if [ "$(hostname -I)" != "" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep $RETRY_EVERY
|
||||||
|
done
|
||||||
|
if [ "$(hostname -I)" = "" ]; then
|
||||||
|
echo 1>&2 -e "\nNo Network After $RETRY_NUM Tries"
|
||||||
|
echo -e "Check Network Settings"
|
||||||
|
exit 101
|
||||||
|
fi
|
||||||
|
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||||
|
systemctl disable -q --now systemd-networkd-wait-online.service
|
||||||
|
}
|
||||||
|
|
||||||
|
network_check() {
|
||||||
|
RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
|
||||||
|
if [[ -z "$RESOLVEDIP" ]]; then msg_error "DNS Lookup Failure"; else msg_ok "DNS Resolved github.com to $RESOLVEDIP"; fi
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
update_os() {
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update >/dev/null 2>&1
|
||||||
|
apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade >/dev/null
|
||||||
|
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||||
|
}
|
||||||
|
|
||||||
|
motd_ssh() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customize() {
|
||||||
|
return
|
||||||
|
}
|
34
.github/workflows/scripts/generate-app-headers.sh
vendored
Normal file
34
.github/workflows/scripts/generate-app-headers.sh
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Base directory for headers
|
||||||
|
headers_dir="./ct/headers"
|
||||||
|
|
||||||
|
# Ensure the headers directory exists and clear it
|
||||||
|
mkdir -p "$headers_dir"
|
||||||
|
rm -f "$headers_dir"/*
|
||||||
|
|
||||||
|
# Find all .sh files in ./ct directory, sorted alphabetically
|
||||||
|
find ./ct -type f -name "*.sh" | sort | while read -r script; do
|
||||||
|
# Extract the APP name from the APP line
|
||||||
|
app_name=$(grep -oP '^APP="\K[^"]+' "$script" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "$app_name" ]]; then
|
||||||
|
# Define the output file name in the headers directory
|
||||||
|
output_file="${headers_dir}/$(basename "${script%.*}")"
|
||||||
|
|
||||||
|
# Generate figlet output
|
||||||
|
figlet_output=$(figlet -w 500 -f slant "$app_name")
|
||||||
|
|
||||||
|
# Check if figlet output is not empty
|
||||||
|
if [[ -n "$figlet_output" ]]; then
|
||||||
|
echo "$figlet_output" > "$output_file"
|
||||||
|
echo "Generated: $output_file"
|
||||||
|
else
|
||||||
|
echo "Figlet failed for $app_name in $script"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No APP name found in $script, skipping."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Completed processing .sh files."
|
20
.github/workflows/scripts/update-json.sh
vendored
Normal file
20
.github/workflows/scripts/update-json.sh
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
FILE=$1
|
||||||
|
TODAY=$(date -u +"%Y-%m-%d")
|
||||||
|
|
||||||
|
if [[ -z "$FILE" ]]; then
|
||||||
|
echo "No file specified. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$FILE" ]]; then
|
||||||
|
echo "File $FILE not found. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DATE_IN_JSON=$(jq -r '.date_created' "$FILE" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ "$DATE_IN_JSON" != "$TODAY" ]]; then
|
||||||
|
jq --arg date "$TODAY" '.date_created = $date' "$FILE" > tmp.json && mv tmp.json "$FILE"
|
||||||
|
fi
|
23
.github/workflows/scripts/update_json_date.sh
vendored
Normal file
23
.github/workflows/scripts/update_json_date.sh
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Verzeichnis, das die JSON-Dateien enthält
|
||||||
|
json_dir="./json/*.json"
|
||||||
|
|
||||||
|
current_date=$(date +"%Y-%m-%d")
|
||||||
|
|
||||||
|
for json_file in $json_dir; do
|
||||||
|
if [[ -f "$json_file" ]]; then
|
||||||
|
current_json_date=$(jq -r '.date_created' "$json_file")
|
||||||
|
|
||||||
|
if [[ "$current_json_date" != "$current_date" ]]; then
|
||||||
|
echo "Updating $json_file with date $current_date"
|
||||||
|
jq --arg date "$current_date" '.date_created = $date' "$json_file" > temp.json && mv temp.json "$json_file"
|
||||||
|
|
||||||
|
git add "$json_file"
|
||||||
|
git commit -m "Update date_created to $current_date in $json_file"
|
||||||
|
else
|
||||||
|
echo "Date in $json_file is already up to date."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
git push origin HEAD
|
131
.github/workflows/update-json-date.yml
vendored
Normal file
131
.github/workflows/update-json-date.yml
vendored
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
name: Update JSON Date
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'json/**.json'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-app-files:
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Generate a token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Generate dynamic branch name
|
||||||
|
id: timestamp
|
||||||
|
run: echo "BRANCH_NAME=pr-update-json-$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set up GH_TOKEN
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "GH_TOKEN=${GH_TOKEN}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2 # Ensure we have the last two commits
|
||||||
|
|
||||||
|
- name: Get Previous Commit
|
||||||
|
id: prev_commit
|
||||||
|
run: |
|
||||||
|
PREV_COMMIT=$(git rev-parse HEAD^)
|
||||||
|
echo "Previous commit: $PREV_COMMIT"
|
||||||
|
echo "prev_commit=$PREV_COMMIT" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Get Newly Added JSON Files
|
||||||
|
id: new_json_files
|
||||||
|
run: |
|
||||||
|
git diff --name-only --diff-filter=A ${{ env.prev_commit }} HEAD | grep '^json/.*\.json$' > new_files.txt || true
|
||||||
|
echo "New files detected:"
|
||||||
|
cat new_files.txt || echo "No new files."
|
||||||
|
|
||||||
|
- name: Disable file mode changes
|
||||||
|
run: git config core.fileMode false
|
||||||
|
|
||||||
|
- name: Set up Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "GitHub Actions"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Change JSON Date
|
||||||
|
id: change-json-date
|
||||||
|
run: |
|
||||||
|
current_date=$(date +"%Y-%m-%d")
|
||||||
|
while IFS= read -r file; do
|
||||||
|
# Skip empty lines
|
||||||
|
[[ -z "$file" ]] && continue
|
||||||
|
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
echo "Processing $file..."
|
||||||
|
current_json_date=$(jq -r '.date_created // empty' "$file")
|
||||||
|
if [[ -z "$current_json_date" || "$current_json_date" != "$current_date" ]]; then
|
||||||
|
echo "Updating $file with date $current_date"
|
||||||
|
jq --arg date "$current_date" '.date_created = $date' "$file" > temp.json && mv temp.json "$file"
|
||||||
|
else
|
||||||
|
echo "Date in $file is already up to date."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: File $file not found!"
|
||||||
|
fi
|
||||||
|
done < new_files.txt
|
||||||
|
rm new_files.txt
|
||||||
|
|
||||||
|
- name: Check if there are any changes
|
||||||
|
run: |
|
||||||
|
echo "Checking for changes..."
|
||||||
|
git add -A # Untracked Dateien aufnehmen
|
||||||
|
git status
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes detected."
|
||||||
|
echo "changed=false" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "Changes detected:"
|
||||||
|
git diff --stat --cached
|
||||||
|
echo "changed=true" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 7: Commit and create PR if changes exist
|
||||||
|
- name: Commit and create PR if changes exist
|
||||||
|
if: env.changed == 'true'
|
||||||
|
run: |
|
||||||
|
|
||||||
|
|
||||||
|
git commit -m "Update date in json"
|
||||||
|
git checkout -b ${{ env.BRANCH_NAME }}
|
||||||
|
git push origin ${{ env.BRANCH_NAME }}
|
||||||
|
|
||||||
|
gh pr create --title "[core] update date in json" \
|
||||||
|
--body "This PR is auto-generated by a GitHub Action to update the date in json." \
|
||||||
|
--head ${{ env.BRANCH_NAME }} \
|
||||||
|
--base main \
|
||||||
|
--label "automated pr"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Approve pull request
|
||||||
|
if: env.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(gh pr list --head "${{ env.BRANCH_NAME }}" --json number --jq '.[].number')
|
||||||
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
|
gh pr review $PR_NUMBER --approve
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: No changes detected
|
||||||
|
if: env.changed == 'false'
|
||||||
|
run: echo "No changes to commit. Workflow completed successfully."
|
161
.github/workflows/validate-filenames.yml
vendored
Normal file
161
.github/workflows/validate-filenames.yml
vendored
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
name: Validate filenames
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
paths:
|
||||||
|
- "ct/*.sh"
|
||||||
|
- "install/*.sh"
|
||||||
|
- "json/*.json"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-files:
|
||||||
|
name: Check changed files
|
||||||
|
runs-on: runner-cluster-htl-set
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Get pull request information
|
||||||
|
if: github.event_name == 'pull_request_target'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
id: pr
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: pullRequest } = await github.rest.pulls.get({
|
||||||
|
...context.repo,
|
||||||
|
pull_number: context.payload.pull_request.number,
|
||||||
|
});
|
||||||
|
return pullRequest;
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Ensure the full history is fetched for accurate diffing
|
||||||
|
ref: ${{ github.event_name == 'pull_request_target' && fromJSON(steps.pr.outputs.result).merge_commit_sha || '' }}
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
run: |
|
||||||
|
if ${{ github.event_name == 'pull_request_target' }}; then
|
||||||
|
echo "files=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ steps.pr.outputs.result && fromJSON(steps.pr.outputs.result).merge_commit_sha }} | xargs)" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | xargs)" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Validate filenames in ct and install directory"
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: check-scripts
|
||||||
|
run: |
|
||||||
|
CHANGED_FILES=$(printf "%s\n" ${{ steps.changed-files.outputs.files }} | { grep -E '^(ct|install)/.*\.sh$' || true; })
|
||||||
|
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in $CHANGED_FILES; do
|
||||||
|
# Datei "ct/create_lxc.sh" explizit überspringen
|
||||||
|
if [[ "$FILE" == "ct/create_lxc.sh" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
BASENAME=$(echo "$(basename "${FILE%.*}")")
|
||||||
|
if [[ ! "$BASENAME" =~ ^[a-z0-9-]+$ ]]; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "Non-compliant filenames found, change to lowercase:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Validate filenames in json directory."
|
||||||
|
if: always() && steps.changed-files.outputs.files != ''
|
||||||
|
id: check-json
|
||||||
|
run: |
|
||||||
|
CHANGED_FILES=$(printf "%s\n" ${{ steps.changed-files.outputs.files }} | { grep -E '^json/.*\.json$' || true; })
|
||||||
|
|
||||||
|
NON_COMPLIANT_FILES=""
|
||||||
|
for FILE in $CHANGED_FILES; do
|
||||||
|
BASENAME=$(echo "$(basename "${FILE%.*}")")
|
||||||
|
if [[ ! "$BASENAME" =~ ^[a-z0-9-]+$ ]]; then
|
||||||
|
NON_COMPLIANT_FILES="$NON_COMPLIANT_FILES $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$NON_COMPLIANT_FILES" ]; then
|
||||||
|
echo "files=$NON_COMPLIANT_FILES" >> $GITHUB_OUTPUT
|
||||||
|
echo "Non-compliant filenames found, change to lowercase:"
|
||||||
|
for FILE in $NON_COMPLIANT_FILES; do
|
||||||
|
echo "$FILE"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post results and comment
|
||||||
|
if: always() && steps.check-scripts.outputs.files != '' && steps.check-json.outputs.files != '' && github.event_name == 'pull_request_target'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const result = "${{ job.status }}" === "success" ? "success" : "failure";
|
||||||
|
const nonCompliantFiles = {
|
||||||
|
script: "${{ steps.check-scripts.outputs.files }}",
|
||||||
|
JSON: "${{ steps.check-json.outputs.files }}",
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueNumber = context.payload.pull_request
|
||||||
|
? context.payload.pull_request.number
|
||||||
|
: null;
|
||||||
|
const commentIdentifier = "validate-filenames";
|
||||||
|
let newCommentBody = `<!-- ${commentIdentifier}-start -->\n### Filename validation\n\n`;
|
||||||
|
|
||||||
|
if (result === "failure") {
|
||||||
|
newCommentBody += ":x: We found issues in the following changed files:\n\n";
|
||||||
|
for (const [check, files] of Object.entries(nonCompliantFiles)) {
|
||||||
|
if (files) {
|
||||||
|
newCommentBody += `**${check.charAt(0).toUpperCase() + check.slice(1)} filename invalid:**\n${files
|
||||||
|
.trim()
|
||||||
|
.split(" ")
|
||||||
|
.map((file) => `- ${file}`)
|
||||||
|
.join("\n")}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newCommentBody +=
|
||||||
|
"Please change the filenames to lowercase and use only alphanumeric characters and dashes.\n";
|
||||||
|
} else {
|
||||||
|
newCommentBody += `:rocket: All files passed filename validation!\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
newCommentBody += `\n\n<!-- ${commentIdentifier}-end -->`;
|
||||||
|
|
||||||
|
if (issueNumber) {
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
...context.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingComment = comments.find(
|
||||||
|
(comment) => comment.user.login === "github-actions[bot]",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingComment) {
|
||||||
|
if (existingComment.body.includes(commentIdentifier)) {
|
||||||
|
const re = new RegExp(String.raw`<!-- ${commentIdentifier}-start -->[\s\S]*?<!-- ${commentIdentifier}-end -->`, "");
|
||||||
|
newCommentBody = existingComment.body.replace(re, newCommentBody);
|
||||||
|
} else {
|
||||||
|
newCommentBody = existingComment.body + '\n\n---\n\n' + newCommentBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
...context.repo,
|
||||||
|
comment_id: existingComment.id,
|
||||||
|
body: newCommentBody,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
...context.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body: newCommentBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
16
.vscode/.editorconfig
vendored
Normal file
16
.vscode/.editorconfig
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
; editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
continuation_indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
tab_width = 2
|
||||||
|
; trim_trailing_whitespace = true ; disabled until files are cleaned up
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"bmalehorn.shell-syntax",
|
||||||
|
"timonwong.shellcheck",
|
||||||
|
"foxundermoon.shell-format"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.func": "shellscript"
|
||||||
|
}
|
||||||
|
}
|
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="#">
|
||||||
|
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/images/logo.png" height="100px" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1 align="center">Changelog - Develop</h1>
|
||||||
|
|
||||||
|
<h3 align="center">All notable changes to this project will be documented in this file.</h3>
|
||||||
|
|
||||||
|
|
||||||
|
## 2025-03-03
|
||||||
|
|
||||||
|
### 🚀 Initial Release
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Proxmox Helper Scripts (CE)
|
Copyright (c) 2021-2025 tteck | community-scripts ORG
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
44
README.md
Normal file
44
README.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# 🚧 ProxmoxVED Helper-Scripts (Development Repository)
|
||||||
|
|
||||||
|
**Warning: This repository is under active development and is not intended for production use. Changes may occur at any time!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 What is this?
|
||||||
|
This repository contains a collection of scripts for managing and automating Proxmox Virtual Environment (Proxmox VE). Originally created by [tteck](https://github.com/tteck), the project is now community-driven and continues to evolve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Development Status
|
||||||
|
- **⚠️ Unstable**: Features may be incomplete or subject to change.
|
||||||
|
- **📢 Community-driven**: Contributions and feedback are welcome.
|
||||||
|
- **🔄 Frequent updates**: Active development means rapid iterations and fixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Get Involved
|
||||||
|
Join the discussion, contribute code, or report issues:
|
||||||
|
- **Discord**: [Join the Proxmox Helper Scripts Discord server](https://discord.gg/UHrpNWGwkH)
|
||||||
|
- **GitHub Issues**: [Report bugs or request features](https://github.com/community-scripts/ProxmoxVED/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❤️ Community and Contributions
|
||||||
|
|
||||||
|
We appreciate any contributions to the project—whether it's bug reports, feature requests, documentation improvements, or spreading the word. Your involvement helps keep the project alive and sustainable.
|
||||||
|
|
||||||
|
## 💖 Donate to Support the Project
|
||||||
|
- **Ko-Fi for Community Edition**: [Donate to support this project](https://ko-fi.com/community_scripts) – Donations go towards maintaining the project, testing infrastructure, and charity (cancer research, hospice care). 30% of the funds will be donated to charity.
|
||||||
|
|
||||||
|
|
||||||
|
## 📜 License
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](LICENSE).
|
||||||
|
|
||||||
|
</br>
|
||||||
|
</br>
|
||||||
|
<p align="center">
|
||||||
|
<i style="font-size: smaller;"><b>Proxmox</b>® is a registered trademark of <a href="https://www.proxmox.com/en/about/company">Proxmox Server Solutions GmbH</a>.</i>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
23
SECURITY.md
Normal file
23
SECURITY.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
## Supported Versions
|
||||||
|
This project currently supports the following versions of Proxmox VE:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 8.3.x | :white_check_mark: |
|
||||||
|
| 8.2.x | :white_check_mark: |
|
||||||
|
| 8.1.x | :white_check_mark: |
|
||||||
|
| 8.0.x | Limited support* ❕|
|
||||||
|
| < 8.0 | :x: |
|
||||||
|
|
||||||
|
*Version 8.0.x has limited support. Security updates may not be provided for all issues in this version.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Security vulnerabilities shouldn’t be reported publicly to prevent potential exploitation. Instead, please report any vulnerabilities privately by reaching out directly to us. You can either join our [Discord server](https://discord.gg/UHrpNWGwkH) and send a direct message to a maintainer or contact us via email at contact@community-scripts.org. Be sure to include a detailed description of the vulnerability and the steps to reproduce it. Thank you for helping us keep our project secure!
|
||||||
|
|
||||||
|
Once a vulnerability has been reported, the project maintainers will review it and acknowledge the report within 7 business days. We will then work to address the vulnerability and provide a fix as soon as possible. Depending on the severity of the issue, a patch may be released immediately or included in the next scheduled update.
|
||||||
|
|
||||||
|
Please note that not all reported vulnerabilities may be accepted. The project maintainers reserve the right to decline a vulnerability report if it is deemed to be a low-risk issue or if it conflicts with the project's design or architecture. In such cases, we will provide an explanation for the decision.
|
||||||
|
|
||||||
|
If you have any questions or concerns about this security policy, please don't hesitate to contact the project maintainers.
|
||||||
|
|
5
api/.env.example
Normal file
5
api/.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
MONGO_USER=
|
||||||
|
MONGO_PASSWORD=
|
||||||
|
MONGO_IP=
|
||||||
|
MONGO_PORT=
|
||||||
|
MONGO_DATABASE=
|
32
api/go.mod
Normal file
32
api/go.mod
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
module proxmox-api
|
||||||
|
|
||||||
|
go 1.23.2
|
||||||
|
|
||||||
|
require go.mongodb.org/mongo-driver v1.17.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
gorm.io/driver/mysql v1.5.7 // indirect
|
||||||
|
gorm.io/driver/postgres v1.5.11 // indirect
|
||||||
|
gorm.io/gorm v1.25.12 // indirect
|
||||||
|
)
|
83
api/go.sum
Normal file
83
api/go.sum
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||||
|
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
|
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||||
|
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
450
api/main.go
Normal file
450
api/main.go
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
// Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
// Author: Michel Roegl-Brunner (michelroegl-brunner)
|
||||||
|
// License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/rs/cors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
var client *mongo.Client
|
||||||
|
var collection *mongo.Collection
|
||||||
|
|
||||||
|
func loadEnv() {
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataModel represents a single document in MongoDB
|
||||||
|
type DataModel struct {
|
||||||
|
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
|
||||||
|
CT_TYPE uint `json:"ct_type" bson:"ct_type"`
|
||||||
|
DISK_SIZE float32 `json:"disk_size" bson:"disk_size"`
|
||||||
|
CORE_COUNT uint `json:"core_count" bson:"core_count"`
|
||||||
|
RAM_SIZE uint `json:"ram_size" bson:"ram_size"`
|
||||||
|
OS_TYPE string `json:"os_type" bson:"os_type"`
|
||||||
|
OS_VERSION string `json:"os_version" bson:"os_version"`
|
||||||
|
DISABLEIP6 string `json:"disableip6" bson:"disableip6"`
|
||||||
|
NSAPP string `json:"nsapp" bson:"nsapp"`
|
||||||
|
METHOD string `json:"method" bson:"method"`
|
||||||
|
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||||
|
PVEVERSION string `json:"pve_version" bson:"pve_version"`
|
||||||
|
STATUS string `json:"status" bson:"status"`
|
||||||
|
RANDOM_ID string `json:"random_id" bson:"random_id"`
|
||||||
|
TYPE string `json:"type" bson:"type"`
|
||||||
|
ERROR string `json:"error" bson:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusModel struct {
|
||||||
|
RANDOM_ID string `json:"random_id" bson:"random_id"`
|
||||||
|
ERROR string `json:"error" bson:"error"`
|
||||||
|
STATUS string `json:"status" bson:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CountResponse struct {
|
||||||
|
TotalEntries int64 `json:"total_entries"`
|
||||||
|
StatusCount map[string]int64 `json:"status_count"`
|
||||||
|
NSAPPCount map[string]int64 `json:"nsapp_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectDatabase initializes the MongoDB connection
|
||||||
|
func ConnectDatabase() {
|
||||||
|
loadEnv()
|
||||||
|
|
||||||
|
mongoURI := fmt.Sprintf("mongodb://%s:%s@%s:%s",
|
||||||
|
os.Getenv("MONGO_USER"),
|
||||||
|
os.Getenv("MONGO_PASSWORD"),
|
||||||
|
os.Getenv("MONGO_IP"),
|
||||||
|
os.Getenv("MONGO_PORT"))
|
||||||
|
|
||||||
|
database := os.Getenv("MONGO_DATABASE")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
client, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to connect to MongoDB!", err)
|
||||||
|
}
|
||||||
|
collection = client.Database(database).Collection("data_models")
|
||||||
|
fmt.Println("Connected to MongoDB on 10.10.10.18")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadJSON handles API requests and stores data as a document in MongoDB
|
||||||
|
func UploadJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var input DataModel
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
input.CreatedAt = time.Now()
|
||||||
|
|
||||||
|
_, err := collection.InsertOne(context.Background(), input)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Received data:", input)
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"message": "Data saved successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus updates the status of a record based on RANDOM_ID
|
||||||
|
func UpdateStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var input StatusModel
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := bson.M{"random_id": input.RANDOM_ID}
|
||||||
|
update := bson.M{"$set": bson.M{"status": input.STATUS, "error": input.ERROR}}
|
||||||
|
|
||||||
|
_, err := collection.UpdateOne(context.Background(), filter, update)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Updated data:", input)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"message": "Record updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDataJSON fetches all data from MongoDB
|
||||||
|
func GetDataJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var records []DataModel
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cursor, err := collection.Find(ctx, bson.M{})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var record DataModel
|
||||||
|
if err := cursor.Decode(&record); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(records)
|
||||||
|
}
|
||||||
|
func GetPaginatedData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||||
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
skip := (page - 1) * limit
|
||||||
|
var records []DataModel
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
options := options.Find().SetSkip(int64(skip)).SetLimit(int64(limit))
|
||||||
|
cursor, err := collection.Find(ctx, bson.M{}, options)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var record DataModel
|
||||||
|
if err := cursor.Decode(&record); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
totalCount, err := collection.CountDocuments(ctx, bson.M{})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCount := make(map[string]int64)
|
||||||
|
nsappCount := make(map[string]int64)
|
||||||
|
|
||||||
|
pipeline := []bson.M{
|
||||||
|
{"$group": bson.M{"_id": "$status", "count": bson.M{"$sum": 1}}},
|
||||||
|
}
|
||||||
|
cursor, err := collection.Aggregate(ctx, pipeline)
|
||||||
|
if err == nil {
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var result struct {
|
||||||
|
ID string `bson:"_id"`
|
||||||
|
Count int64 `bson:"count"`
|
||||||
|
}
|
||||||
|
if err := cursor.Decode(&result); err == nil {
|
||||||
|
statusCount[result.ID] = result.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline = []bson.M{
|
||||||
|
{"$group": bson.M{"_id": "$nsapp", "count": bson.M{"$sum": 1}}},
|
||||||
|
}
|
||||||
|
cursor, err = collection.Aggregate(ctx, pipeline)
|
||||||
|
if err == nil {
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var result struct {
|
||||||
|
ID string `bson:"_id"`
|
||||||
|
Count int64 `bson:"count"`
|
||||||
|
}
|
||||||
|
if err := cursor.Decode(&result); err == nil {
|
||||||
|
nsappCount[result.ID] = result.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := CountResponse{
|
||||||
|
TotalEntries: totalCount,
|
||||||
|
StatusCount: statusCount,
|
||||||
|
NSAPPCount: nsappCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetByNsapp(w http.ResponseWriter, r *http.Request) {
|
||||||
|
nsapp := r.URL.Query().Get("nsapp")
|
||||||
|
var records []DataModel
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cursor, err := collection.Find(ctx, bson.M{"nsapp": nsapp})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var record DataModel
|
||||||
|
if err := cursor.Decode(&record); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetByDateRange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
startDate := r.URL.Query().Get("start_date")
|
||||||
|
endDate := r.URL.Query().Get("end_date")
|
||||||
|
|
||||||
|
if startDate == "" || endDate == "" {
|
||||||
|
http.Error(w, "Both start_date and end_date are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := time.Parse("2006-01-02T15:04:05.999999+00:00", startDate+"T00:00:00+00:00")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid start_date format", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := time.Parse("2006-01-02T15:04:05.999999+00:00", endDate+"T23:59:59+00:00")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid end_date format", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []DataModel
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cursor, err := collection.Find(ctx, bson.M{
|
||||||
|
"created_at": bson.M{
|
||||||
|
"$gte": start,
|
||||||
|
"$lte": end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var record DataModel
|
||||||
|
if err := cursor.Decode(&record); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(records)
|
||||||
|
}
|
||||||
|
func GetByStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
status := r.URL.Query().Get("status")
|
||||||
|
var records []DataModel
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cursor, err := collection.Find(ctx, bson.M{"status": status})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var record DataModel
|
||||||
|
if err := cursor.Decode(&record); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetByOS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
osType := r.URL.Query().Get("os_type")
|
||||||
|
osVersion := r.URL.Query().Get("os_version")
|
||||||
|
var records []DataModel
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cursor, err := collection.Find(ctx, bson.M{"os_type": osType, "os_version": osVersion})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var record DataModel
|
||||||
|
if err := cursor.Decode(&record); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetErrors(w http.ResponseWriter, r *http.Request) {
|
||||||
|
errorCount := make(map[string]int)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cursor, err := collection.Find(ctx, bson.M{"error": bson.M{"$ne": ""}})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var record DataModel
|
||||||
|
if err := cursor.Decode(&record); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.ERROR != "" {
|
||||||
|
errorCount[record.ERROR]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorCountResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorCounts []ErrorCountResponse
|
||||||
|
for err, count := range errorCount {
|
||||||
|
errorCounts = append(errorCounts, ErrorCountResponse{
|
||||||
|
Error: err,
|
||||||
|
Count: count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(struct {
|
||||||
|
ErrorCounts []ErrorCountResponse `json:"error_counts"`
|
||||||
|
}{
|
||||||
|
ErrorCounts: errorCounts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ConnectDatabase()
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/upload", UploadJSON).Methods("POST")
|
||||||
|
router.HandleFunc("/upload/updatestatus", UpdateStatus).Methods("POST")
|
||||||
|
router.HandleFunc("/data/json", GetDataJSON).Methods("GET")
|
||||||
|
router.HandleFunc("/data/paginated", GetPaginatedData).Methods("GET")
|
||||||
|
router.HandleFunc("/data/summary", GetSummary).Methods("GET")
|
||||||
|
router.HandleFunc("/data/nsapp", GetByNsapp).Methods("GET")
|
||||||
|
router.HandleFunc("/data/date", GetByDateRange).Methods("GET")
|
||||||
|
router.HandleFunc("/data/status", GetByStatus).Methods("GET")
|
||||||
|
router.HandleFunc("/data/os", GetByOS).Methods("GET")
|
||||||
|
router.HandleFunc("/data/errors", GetErrors).Methods("GET")
|
||||||
|
|
||||||
|
c := cors.New(cors.Options{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
|
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := c.Handler(router)
|
||||||
|
|
||||||
|
fmt.Println("Server running on port 8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", handler))
|
||||||
|
}
|
38
ct/alpine.sh
Normal file
38
ct/alpine.sh
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2025 tteck
|
||||||
|
# Author: tteck (tteckster)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
# Source: https://alpinelinux.org/
|
||||||
|
|
||||||
|
APP="Alpine"
|
||||||
|
var_tags="os;alpine"
|
||||||
|
var_cpu="1"
|
||||||
|
var_ram="512"
|
||||||
|
var_disk="0.1"
|
||||||
|
var_os="alpine"
|
||||||
|
var_version="3.21"
|
||||||
|
var_unprivileged="1"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
UPD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SUPPORT" --radiolist --cancel-button Exit-Script "Spacebar = Select" 11 58 1 \
|
||||||
|
"1" "Check for Alpine Updates" ON \
|
||||||
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
header_info
|
||||||
|
if [ "$UPD" == "1" ]; then
|
||||||
|
apk update && apk upgrade
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
42
ct/debian.sh
Normal file
42
ct/debian.sh
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2025 tteck
|
||||||
|
# Author: tteck (tteckster)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
# Source: https://www.debian.org/
|
||||||
|
|
||||||
|
APP="Debian"
|
||||||
|
var_tags="os"
|
||||||
|
var_cpu="1"
|
||||||
|
var_ram="512"
|
||||||
|
var_disk="2"
|
||||||
|
var_os="debian"
|
||||||
|
var_version="12"
|
||||||
|
var_unprivileged="1"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
if [[ ! -d /var ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
msg_info "Updating $APP LXC"
|
||||||
|
$STD apt-get update
|
||||||
|
$STD apt-get -y upgrade
|
||||||
|
msg_ok "Updated $APP LXC"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
42
ct/ubuntu.sh
Normal file
42
ct/ubuntu.sh
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2025 tteck
|
||||||
|
# Author: tteck (tteckster)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||||
|
# Source: https://ubuntu.com/
|
||||||
|
|
||||||
|
echo -e "Loading..."
|
||||||
|
APP="Ubuntu"
|
||||||
|
var_tags="os"
|
||||||
|
var_cpu="1"
|
||||||
|
var_ram="512"
|
||||||
|
var_disk="2"
|
||||||
|
var_os="ubuntu"
|
||||||
|
var_version="24.04"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
if [[ ! -d /var ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
msg_info "Updating ${APP} LXC"
|
||||||
|
$STD apt-get update
|
||||||
|
$STD apt-get -y upgrade
|
||||||
|
msg_ok "Updated ${APP} LXC"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
5
frontend/.eslintrc.json
Normal file
5
frontend/.eslintrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint"]
|
||||||
|
}
|
39
frontend/.gitignore
vendored
Normal file
39
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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
|
5
frontend/.prettierignore
Normal file
5
frontend/.prettierignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
build
|
||||||
|
.contentlayer
|
3
frontend/.prettierrc
Normal file
3
frontend/.prettierrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-organize-imports"]
|
||||||
|
}
|
21
frontend/LICENSE
Normal file
21
frontend/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
Normal file
17
frontend/components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
25
frontend/next.config.mjs
Normal file
25
frontend/next.config.mjs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
webpack: (config) => {
|
||||||
|
config.resolve.alias.canvas = false;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
env: {
|
||||||
|
BASE_PATH: "ProxmoxVE",
|
||||||
|
},
|
||||||
|
|
||||||
|
output: "export",
|
||||||
|
basePath: `/ProxmoxVE`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
10147
frontend/package-lock.json
generated
Normal file
10147
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
92
frontend/package.json
Normal file
92
frontend/package.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"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.1.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
"@radix-ui/react-icons": "^1.3.1",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||||
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@vercel/analytics": "^1.2.2",
|
||||||
|
"chart.js": "^4.4.1",
|
||||||
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.0.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"framer-motion": "^11.11.11",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
|
"lucide-react": "^0.453.0",
|
||||||
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
|
"next": "15.1.3",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"nuqs": "^2.1.1",
|
||||||
|
"pocketbase": "^0.21.4",
|
||||||
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
|
"react": "19.0.0-rc-02c0e824-20241028",
|
||||||
|
"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-rc-02c0e824-20241028",
|
||||||
|
"react-icons": "^5.1.0",
|
||||||
|
"react-simple-typewriter": "^5.0.1",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"simple-icons": "^13.5.0",
|
||||||
|
"sonner": "^1.5.0",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
"@testing-library/react": "^16.0.1",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@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.8.1",
|
||||||
|
"@typescript-eslint/parser": "^8.8.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"eslint-config-next": "15.0.2",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"postcss": "^8",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
|
"tailwindcss": "^3.4.9",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tailwindcss-animated": "^1.1.2",
|
||||||
|
"typescript": "^5",
|
||||||
|
"vite-tsconfig-paths": "^5.1.3",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
}
|
8
frontend/postcss.config.mjs
Normal file
8
frontend/postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
BIN
frontend/public/defaultimg.png
Normal file
BIN
frontend/public/defaultimg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
1
frontend/public/json
Normal file
1
frontend/public/json
Normal file
@ -0,0 +1 @@
|
|||||||
|
../../json
|
BIN
frontend/public/logo.png
Normal file
BIN
frontend/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
11
frontend/src/__tests__/app/page.test.tsx
Normal file
11
frontend/src/__tests__/app/page.test.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
53
frontend/src/__tests__/public/validate-json.test.ts
Normal file
53
frontend/src/__tests__/public/validate-json.test.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
const jsonDir = "public/json";
|
||||||
|
const metadataFileName = "metadata.json";
|
||||||
|
const encoding = "utf-8";
|
||||||
|
|
||||||
|
const fileNames = (await fs.readdir(jsonDir))
|
||||||
|
.filter((fileName) => fileName !== metadataFileName)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
4
frontend/src/__tests__/setupTests.ts
Normal file
4
frontend/src/__tests__/setupTests.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
// Mock canvas getContext
|
||||||
|
HTMLCanvasElement.prototype.getContext = vi.fn();
|
56
frontend/src/app/api/categories/route.ts
Normal file
56
frontend/src/app/api/categories/route.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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);
|
||||||
|
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)
|
||||||
|
.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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
276
frontend/src/app/category-view/page.tsx
Normal file
276
frontend/src/app/category-view/page.tsx
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
"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" ? "/ProxmoxVE" : "";
|
||||||
|
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;
|
198
frontend/src/app/data/page.tsx
Normal file
198
frontend/src/app/data/page.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
"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/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/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>
|
||||||
|
<ApplicationChart data={summary} />
|
||||||
|
<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;
|
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
117
frontend/src/app/json-editor/_components/Categories.tsx
Normal file
117
frontend/src/app/json-editor/_components/Categories.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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);
|
240
frontend/src/app/json-editor/_components/InstallMethod.tsx
Normal file
240
frontend/src/app/json-editor/_components/InstallMethod.tsx
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
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 method = InstallMethodSchema.parse({
|
||||||
|
type: "default",
|
||||||
|
script: `${prev.type}/${prev.slug}.sh`,
|
||||||
|
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);
|
130
frontend/src/app/json-editor/_components/Note.tsx
Normal file
130
frontend/src/app/json-editor/_components/Note.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
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]);
|
||||||
|
|
||||||
|
const NoteItem = memo(
|
||||||
|
({ note, index }: { note: Script["notes"][number]; index: number }) => (
|
||||||
|
<div className="space-y-2 border p-4 rounded">
|
||||||
|
<Input
|
||||||
|
placeholder="Note Text"
|
||||||
|
value={note.text}
|
||||||
|
onChange={(e) => updateNote(index, "text", e.target.value)}
|
||||||
|
ref={(el) => {
|
||||||
|
inputRefs.current[index] = el;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="text-xl font-semibold">Notes</h3>
|
||||||
|
{script.notes.map((note, index) => (
|
||||||
|
<NoteItem key={index} note={note} index={index} />
|
||||||
|
))}
|
||||||
|
<Button type="button" size="sm" onClick={addNote}>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Note);
|
45
frontend/src/app/json-editor/_schemas/schemas.ts
Normal file
45
frontend/src/app/json-editor/_schemas/schemas.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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", "misc", "turnkey"], {
|
||||||
|
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'misc' 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"),
|
||||||
|
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>;
|
355
frontend/src/app/json-editor/page.tsx
Normal file
355
frontend/src/app/json-editor/page.tsx
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
"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,
|
||||||
|
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 (key === "type" || key === "slug") {
|
||||||
|
updated.install_methods = updated.install_methods.map((method) => ({
|
||||||
|
...method,
|
||||||
|
script:
|
||||||
|
method.type === "alpine"
|
||||||
|
? `${updated.type}/alpine-${updated.slug}.sh`
|
||||||
|
: `${updated.type}/${updated.slug}.sh`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
<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="misc">Miscellaneous</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>
|
||||||
|
);
|
||||||
|
}
|
94
frontend/src/app/layout.tsx
Normal file
94
frontend/src/app/layout.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import Footer from "@/components/Footer";
|
||||||
|
import Navbar from "@/components/Navbar";
|
||||||
|
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",
|
||||||
|
generator: "Next.js",
|
||||||
|
applicationName: "Proxmox VE Helper-Scripts",
|
||||||
|
referrer: "origin-when-cross-origin",
|
||||||
|
keywords: [
|
||||||
|
"Proxmox VE",
|
||||||
|
"Helper-Scripts",
|
||||||
|
"tteck",
|
||||||
|
"helper",
|
||||||
|
"scripts",
|
||||||
|
"proxmox",
|
||||||
|
"VE",
|
||||||
|
],
|
||||||
|
authors: { name: "Bram Suurd" },
|
||||||
|
creator: "Bram Suurd",
|
||||||
|
publisher: "Bram Suurd",
|
||||||
|
description:
|
||||||
|
"A Front-end for the Proxmox VE Helper-Scripts (Community) 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",
|
||||||
|
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 ">
|
||||||
|
<NuqsAdapter>{children}</NuqsAdapter>
|
||||||
|
<Toaster richColors />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
28
frontend/src/app/manifest.ts
Normal file
28
frontend/src/app/manifest.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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",
|
||||||
|
short_name: "Proxmox VE Helper-Scripts",
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
20
frontend/src/app/not-found.tsx
Normal file
20
frontend/src/app/not-found.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
137
frontend/src/app/page.tsx
Normal file
137
frontend/src/app/page.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"use client";
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
14
frontend/src/app/robots.ts
Normal file
14
frontend/src/app/robots.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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`,
|
||||||
|
};
|
||||||
|
}
|
132
frontend/src/app/scripts/_components/ScriptAccordion.tsx
Normal file
132
frontend/src/app/scripts/_components/ScriptAccordion.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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-220px)] 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>
|
||||||
|
);
|
||||||
|
}
|
223
frontend/src/app/scripts/_components/ScriptInfoBlocks.tsx
Normal file
223
frontend/src/app/scripts/_components/ScriptInfoBlocks.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
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 "misc":
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
107
frontend/src/app/scripts/_components/ScriptItem.tsx
Normal file
107
frontend/src/app/scripts/_components/ScriptItem.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
"use client";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { extractDate } from "@/lib/time";
|
||||||
|
import { Script } from "@/lib/types";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
|
||||||
|
import Alerts from "./ScriptItems/Alerts";
|
||||||
|
import Buttons from "./ScriptItems/Buttons";
|
||||||
|
import DefaultPassword from "./ScriptItems/DefaultPassword";
|
||||||
|
import DefaultSettings from "./ScriptItems/DefaultSettings";
|
||||||
|
import Description from "./ScriptItems/Description";
|
||||||
|
import InstallCommand from "./ScriptItems/InstallCommand";
|
||||||
|
import InterFaces from "./ScriptItems/InterFaces";
|
||||||
|
import Tooltips from "./ScriptItems/Tooltips";
|
||||||
|
import { basePath } from "@/config/siteConfig";
|
||||||
|
|
||||||
|
function ScriptItem({
|
||||||
|
item,
|
||||||
|
setSelectedScript,
|
||||||
|
}: {
|
||||||
|
item: Script;
|
||||||
|
setSelectedScript: (script: string | null) => void;
|
||||||
|
}) {
|
||||||
|
const closeScript = () => {
|
||||||
|
window.history.pushState({}, document.title, window.location.pathname);
|
||||||
|
setSelectedScript(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultInstallMethod = item.install_methods?.[0];
|
||||||
|
const os = defaultInstallMethod?.resources?.os || "Proxmox Node";
|
||||||
|
const version = defaultInstallMethod?.resources?.version || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mr-7 mt-0 flex w-full min-w-fit">
|
||||||
|
<div className="flex w-full min-w-fit">
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<div className="flex h-[36px] min-w-max items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold">Selected Script</h2>
|
||||||
|
<X onClick={closeScript} className="cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-accent/20 p-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<Image
|
||||||
|
className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
|
||||||
|
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 className="ml-4 flex flex-col justify-between">
|
||||||
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold">
|
||||||
|
{item.name} {getDisplayValueFromType(item.type)}
|
||||||
|
</h1>
|
||||||
|
<p className="w-full text-sm text-muted-foreground">
|
||||||
|
Date added: {extractDate(item.date_created)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Default OS: {os} {version}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-5">
|
||||||
|
<DefaultSettings item={item} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden flex-col justify-between gap-2 sm:flex">
|
||||||
|
<InterFaces item={item} />
|
||||||
|
<Buttons item={item} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="mt-4" />
|
||||||
|
<div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Description item={item} />
|
||||||
|
<Alerts item={item} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 rounded-lg border bg-accent/50">
|
||||||
|
<div className="flex gap-3 px-4 py-2">
|
||||||
|
<h2 className="text-lg font-semibold">
|
||||||
|
How to {item.type == "misc" ? "use" : "install"}
|
||||||
|
</h2>
|
||||||
|
<Tooltips item={item} />
|
||||||
|
</div>
|
||||||
|
<Separator className="w-full"></Separator>
|
||||||
|
<InstallCommand item={item} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DefaultPassword item={item} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScriptItem;
|
35
frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx
Normal file
35
frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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 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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
81
frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx
Normal file
81
frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { basePath } from "@/config/siteConfig";
|
||||||
|
import { Script } from "@/lib/types";
|
||||||
|
import { BookOpenText, Code, Globe, RefreshCcw } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
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`;
|
||||||
|
return type === "vm" ? `${baseUrl}/vm/${slug}.sh` : `${baseUrl}/misc/${slug}.sh`;
|
||||||
|
return `${baseUrl}/misc/${slug}.sh`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUpdateUrl = (slug: string) => {
|
||||||
|
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||||
|
return `${baseUrl}/ct/${slug}.sh`;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ButtonLinkProps {
|
||||||
|
href: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonLink = ({ href, icon, text }: ButtonLinkProps) => (
|
||||||
|
<Button variant="secondary" asChild>
|
||||||
|
<Link target="_blank" href={href}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{icon}
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
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) : null;
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
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 ButtonLinkProps[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap justify-end gap-2">
|
||||||
|
{buttons.map((props, index) => (
|
||||||
|
<ButtonLink key={index} {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
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 bg-accent/50">
|
||||||
|
<div className="flex gap-3 px-4 py-2">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
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 (
|
||||||
|
<>
|
||||||
|
{hasDefaultSettings && (
|
||||||
|
<ResourceDisplay settings={defaultSettings} title="Default settings" />
|
||||||
|
)}
|
||||||
|
{defaultAlpineSettings && (
|
||||||
|
<ResourceDisplay
|
||||||
|
settings={defaultAlpineSettings}
|
||||||
|
title="Default Alpine settings"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
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?: string, isAlpine = false) => {
|
||||||
|
return `bash -c "$(wget -q${isAlpine ? "" : "L"}O - https://github.com/community-scripts/${basePath}/raw/main/${scriptPath})"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 == "misc" ? (
|
||||||
|
<>
|
||||||
|
To use the {item.name} script, run the command below in the shell.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
const CopyButton = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
}) => (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ size: "sm", variant: "secondary" }),
|
||||||
|
"flex items-center gap-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
<ClipboardIcon
|
||||||
|
onClick={() => handleCopy(label, String(value))}
|
||||||
|
className="size-4 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function InterFaces({ item }: { item: Script }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{item.interface_port !== null ? (
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<h2 className="mr-2 text-end text-lg font-semibold">
|
||||||
|
{"Default Interface:"}
|
||||||
|
</h2>{" "}
|
||||||
|
<CopyButton label="default interface" value={item.interface_port} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user