Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0220f338db | |||
| 985f04c7ad | |||
| 8b3cde79cf | |||
| 72d933228c | |||
| 5999183c8e | |||
| 286576715b | |||
| 820da8ca01 | |||
| a275b8bed5 | |||
| fbfbd76960 | |||
| 8fa1208cde |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -4,6 +4,20 @@ All notable changes to PVEmanager will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [v1.16.1] - 2026-08-17
|
||||
|
||||
### 🎨 UI
|
||||
|
||||
- **Inter Variable font** — the frontend now uses the variable-weight Inter font with a bumped type scale for sharper, more consistent typography across the panel
|
||||
|
||||
### 🔧 Deployment Fixes
|
||||
|
||||
- **`pve` CLI installs without root** — `deploy.sh` now tries `sudo` to install the CLI to `/usr/local/bin`, and falls back to `~/.local/bin` (with automatic PATH configuration) when root access is unavailable; previously the auto-install was silently skipped, leaving the `pve` command unavailable after deploy
|
||||
- **`pve` CLI file permissions** — the installed script is now set to `755` instead of relying on umask; previously `sudo cp` could leave the file without read permission for non-root users, causing "Permission denied" on every invocation
|
||||
- **SSL no longer rolls back on deploy** — `verify_nginx_running` now uses `docker compose ps --format '{{.State}}'` (returns `"running"` immediately) instead of grepping the human-readable STATUS column (which shows `"Up Xs (health: starting)"` for 30 s before the first healthcheck); with the old 20 s timeout the function always timed out, rolling back to HTTP even though the SSL certificate was successfully obtained
|
||||
|
||||
---
|
||||
|
||||
## [v1.16.0] - 2026-08-14
|
||||
|
||||
### 🌍 i18n — Full Translation Pass
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
|
||||
---
|
||||
|
||||
## [v1.16.1] - 2026-08-17
|
||||
|
||||
### 🎨 Интерфейс
|
||||
|
||||
- **Шрифт Inter Variable** — фронтенд переведён на вариативный шрифт Inter с увеличенной шкалой размеров для более чёткой и единообразной типографики по всей панели
|
||||
|
||||
### 🔧 Исправления деплоя
|
||||
|
||||
- **CLI `pve` устанавливается без root** — `deploy.sh` теперь пробует `sudo` для установки CLI в `/usr/local/bin`, а при недоступности root откатывается на `~/.local/bin` (с автоматической настройкой PATH); ранее авто-установка молча пропускалась, и команда `pve` оставалась недоступной после деплоя
|
||||
- **Права файла CLI `pve`** — установленный скрипт теперь явно получает права `755` вместо зависимости от umask; ранее `sudo cp` мог оставить файл без права на чтение для не-root пользователей, вызывая «Permission denied» при каждом запуске
|
||||
- **SSL больше не откатывается при деплое** — `verify_nginx_running` теперь использует `docker compose ps --format '{{.State}}'` (возвращает `"running"` мгновенно) вместо grep по человекочитаемому столбцу STATUS (который показывает `"Up Xs (health: starting)"` первые 30 секунд до первого healthcheck); со старым таймаутом 20 секунд функция всегда завершалась по таймауту и откатывала конфиг на HTTP, хотя SSL-сертификат был успешно получен
|
||||
|
||||
---
|
||||
|
||||
## [v1.16.0] - 2026-08-14
|
||||
|
||||
### 🌍 i18n — Полный проход по переводам
|
||||
|
||||
180
CONTRIBUTING.md
Normal file
180
CONTRIBUTING.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Contributing to PVEmanager
|
||||
|
||||
Thank you for considering contributing to PVEmanager! This guide will help you get started.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Tool | Version |
|
||||
|---|---|
|
||||
| Docker & Docker Compose | 24.x+ / v2.x+ |
|
||||
| Node.js | 20+ |
|
||||
| Python | 3.12+ |
|
||||
| Git | 2.x+ |
|
||||
|
||||
You also need access to at least one Proxmox VE host (7.x / 8.x / 9.x) for integration testing.
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://git.tzim.uz/markmorado/pvemanager.git
|
||||
cd pvemanager
|
||||
|
||||
# Copy environment files
|
||||
cp .env.example .env
|
||||
cp backend/.env.example backend/.env
|
||||
|
||||
# Start everything with Docker
|
||||
docker compose up -d
|
||||
|
||||
# Or use dev mode with Vite HMR for frontend:
|
||||
docker compose -f compose.yml -f compose.dev.yml up -d frontend
|
||||
```
|
||||
|
||||
The panel is available at `http://localhost:3001` (default login: `admin` / `admin123`).
|
||||
|
||||
### Frontend (local development)
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # Vite dev server with HMR
|
||||
npm run lint # ESLint
|
||||
npm run build:check # TypeScript check + build
|
||||
```
|
||||
|
||||
### Backend (local development)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
||||
pip install -r requirements-dev.txt
|
||||
pytest # run test suite
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pvemanager/
|
||||
├── backend/ # Python 3.12 + FastAPI + SQLAlchemy
|
||||
│ ├── app/ # Application code
|
||||
│ ├── alembic/ # Database migration config
|
||||
│ ├── migrations/ # Alembic migration scripts
|
||||
│ └── tests/ # pytest tests
|
||||
├── frontend/ # React 19 + TypeScript + Vite 8
|
||||
│ └── src/
|
||||
│ ├── features/ # Feature modules
|
||||
│ └── lib/ # Shared utilities
|
||||
├── nginx/ # Reverse proxy config
|
||||
├── systemd/ # Systemd service files
|
||||
└── compose.yml # Docker Compose (production)
|
||||
```
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
- `main` is the primary branch. All pull requests target `main`.
|
||||
- Use descriptive branch names: `feat/ssh-key-rotation`, `fix/vnc-reconnect`, `i18n/missing-keys`.
|
||||
|
||||
## Commit Messages
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/). Write commit messages **in English**.
|
||||
|
||||
Format: `<type>(<optional scope>): <description>`
|
||||
|
||||
| Type | Use for |
|
||||
|---|---|
|
||||
| `feat` | New features |
|
||||
| `fix` | Bug fixes |
|
||||
| `docs` | Documentation changes |
|
||||
| `i18n` | Translations |
|
||||
| `refactor` | Code changes that neither fix bugs nor add features |
|
||||
| `test` | Adding or updating tests |
|
||||
| `scripts` | Shell scripts, tooling |
|
||||
| `chore` | Maintenance, dependencies |
|
||||
|
||||
Examples:
|
||||
```
|
||||
feat(frontend): add bulk snapshot dialog
|
||||
fix: prevent SSL rollback by using container state
|
||||
i18n: translate dashboard and action dialogs
|
||||
docs: release notes and version bump for v1.16.1
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### Backend (Python)
|
||||
- Follow PEP 8.
|
||||
- Use type hints for function signatures.
|
||||
- Keep imports sorted: stdlib, third-party, local.
|
||||
|
||||
### Frontend (TypeScript / React)
|
||||
- Run `npm run lint` before committing.
|
||||
- Use functional components with hooks.
|
||||
- UI components: [shadcn/ui](https://ui.shadcn.com/) + Tailwind CSS v4.
|
||||
- State management: Zustand for global state, TanStack Query for server state.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
The UI supports Russian and English. Translation files live under `frontend/src/` and use `i18next`.
|
||||
|
||||
- Every user-facing string must have a translation key.
|
||||
- Add keys to both `en` and `ru` translation files.
|
||||
- Use the `useTranslation()` hook, not hardcoded strings.
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend
|
||||
```bash
|
||||
cd backend
|
||||
pytest # all tests
|
||||
pytest -m smoke # smoke tests only
|
||||
pytest -m unit # unit tests only
|
||||
pytest -m integration # integration tests
|
||||
pytest --cov # with coverage
|
||||
```
|
||||
|
||||
### Frontend
|
||||
```bash
|
||||
cd frontend
|
||||
npm run lint # ESLint checks
|
||||
npm run build:check # TypeScript type checking + build
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
We use Alembic for database schema changes.
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
alembic revision --autogenerate -m "describe your change"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
Always test migrations both up and down before submitting.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. Make your changes, following the code style and commit conventions above.
|
||||
3. Add or update tests if applicable.
|
||||
4. Update translations if you added user-facing strings.
|
||||
5. Run the full test suite and linter.
|
||||
6. Open a pull request with a clear description of what and why.
|
||||
|
||||
### PR Checklist
|
||||
|
||||
- [ ] Commit messages follow Conventional Commits (in English)
|
||||
- [ ] Linter passes (`npm run lint` / PEP 8)
|
||||
- [ ] Tests pass (`pytest` / `npm run build:check`)
|
||||
- [ ] New strings have i18n keys in both `en` and `ru`
|
||||
- [ ] Database migration included (if schema changed)
|
||||
- [ ] No secrets or credentials in the diff
|
||||
|
||||
## Security
|
||||
|
||||
If you discover a security vulnerability, **do not** open a public issue. Instead, refer to [SECURITY.md](SECURITY.md) for responsible disclosure instructions.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
||||
12
README.md
12
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
**Modern web panel for managing Proxmox servers, virtual machines and LXC containers**
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](LICENSE)
|
||||
[](compose.yml)
|
||||
[](backend/)
|
||||
@@ -165,16 +165,14 @@ Configured via the web interface — **Settings → Notifications**:
|
||||
|---|---|
|
||||
| [WIKI.md](WIKI.md) | Full user and administrator guide |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | Version history and release notes |
|
||||
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to contribute — setup, code style, PR process |
|
||||
| [SECURITY.md](SECURITY.md) | Security policy and vulnerability reporting |
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/my-feature`
|
||||
3. Commit your changes: `git commit -m 'feat: add my feature'`
|
||||
4. Push: `git push origin feature/my-feature`
|
||||
5. Open a Pull Request
|
||||
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, code style, commit conventions, and the PR process.
|
||||
|
||||
---
|
||||
|
||||
@@ -188,6 +186,6 @@ Configured via the web interface — **Settings → Notifications**:
|
||||
|
||||
Made with ❤️ for Proxmox users
|
||||
|
||||
[Issues](https://git.tzim.uz/markmorado/pvemanager/issues) · [Changelog](CHANGELOG.md) · [Wiki](WIKI.md)
|
||||
[Issues](https://git.tzim.uz/markmorado/pvemanager/issues) · [Changelog](CHANGELOG.md) · [Wiki](WIKI.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
|
||||
|
||||
</div>
|
||||
|
||||
12
README.ru.md
12
README.ru.md
@@ -4,7 +4,7 @@
|
||||
|
||||
**Современная веб-панель для управления серверами Proxmox, виртуальными машинами и контейнерами LXC**
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](LICENSE)
|
||||
[](compose.yml)
|
||||
[](backend/)
|
||||
@@ -165,16 +165,14 @@ GITHUB_TOKEN= # опционально, для провер
|
||||
|---|---|
|
||||
| [WIKI.md](WIKI.md) | Полное руководство пользователя и администратора |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | История версий и примечания к выпускам |
|
||||
| [CONTRIBUTING.md](CONTRIBUTING.md) | Как внести вклад — настройка, стиль кода, процесс PR |
|
||||
| [SECURITY.md](SECURITY.md) | Политика безопасности и сообщение об уязвимостях |
|
||||
|
||||
---
|
||||
|
||||
## Участие в разработке
|
||||
|
||||
1. Сделайте форк репозитория
|
||||
2. Создайте ветку функции: `git checkout -b feature/my-feature`
|
||||
3. Зафиксируйте изменения: `git commit -m 'feat: добавьте мою функцию'`
|
||||
4. Отправьте: `git push origin feature/my-feature`
|
||||
5. Откройте Pull Request
|
||||
Мы рады вашему вкладу! См. [CONTRIBUTING.md](CONTRIBUTING.md) — там описаны настройка окружения, стиль кода, формат коммитов и процесс PR.
|
||||
|
||||
---
|
||||
|
||||
@@ -188,6 +186,6 @@ GITHUB_TOKEN= # опционально, для провер
|
||||
|
||||
Сделано с ❤️ для пользователей Proxmox
|
||||
|
||||
[Проблемы](https://git.tzim.uz/markmorado/pvemanager/issues) · [История изменений](CHANGELOG.md) · [Вики](WIKI.md)
|
||||
[Проблемы](https://git.tzim.uz/markmorado/pvemanager/issues) · [История изменений](CHANGELOG.md) · [Вики](WIKI.md) · [Участие](CONTRIBUTING.md) · [Безопасность](SECURITY.md)
|
||||
|
||||
</div>
|
||||
|
||||
64
SECURITY.md
64
SECURITY.md
@@ -2,20 +2,60 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
| Version | Supported |
|
||||
|---|---|
|
||||
| 1.16.x (latest) | :white_check_mark: |
|
||||
| < 1.16 | :x: |
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
Only the latest minor release receives security patches. We recommend always running the most recent version.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
**Do not open a public issue for security vulnerabilities.**
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
Please report them privately via email: **rasulovd9309@gmail.com**
|
||||
|
||||
Include as much detail as possible:
|
||||
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Affected component (backend, frontend, nginx, Docker config)
|
||||
- Potential impact
|
||||
- Suggested fix (if any)
|
||||
|
||||
### What to Expect
|
||||
|
||||
- **Acknowledgement** within 48 hours.
|
||||
- **Status update** within 7 days with an initial assessment.
|
||||
- If confirmed, a fix will be developed and released as a patch version. You will be credited in the release notes (unless you prefer to remain anonymous).
|
||||
- If declined, we will explain why.
|
||||
|
||||
## Scope
|
||||
|
||||
The following are in scope:
|
||||
|
||||
- Authentication and authorization bypass
|
||||
- SQL injection, command injection, SSRF
|
||||
- Credential or secret exposure
|
||||
- Cross-site scripting (XSS) and CSRF
|
||||
- Proxmox API token or session leakage
|
||||
- Container escape or privilege escalation
|
||||
- Insecure default configurations
|
||||
|
||||
Out of scope:
|
||||
|
||||
- Vulnerabilities in Proxmox VE itself (report to [Proxmox](https://www.proxmox.com/en/about/security))
|
||||
- Denial-of-service attacks against self-hosted instances
|
||||
- Issues requiring physical access to the host
|
||||
|
||||
## Security Design
|
||||
|
||||
PVEmanager applies the following security measures:
|
||||
|
||||
- **Encrypted credentials** — Proxmox API tokens and sensitive settings are Fernet-encrypted at rest in PostgreSQL.
|
||||
- **Bcrypt passwords** — user passwords are hashed with bcrypt, never stored in plaintext.
|
||||
- **JWT sessions** — short-lived tokens with server-side revocation support.
|
||||
- **2FA** — optional TOTP-based two-factor authentication per user.
|
||||
- **RBAC** — granular role-based access control enforced on both server and client side.
|
||||
- **Unprivileged LXC** — App Store containers run unprivileged by design.
|
||||
- **Injection-safe exec** — `pct exec` / `pct push` calls are parameterized to prevent shell injection.
|
||||
|
||||
117
deploy.sh
117
deploy.sh
@@ -6,6 +6,10 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# Save script path and original args for potential re-exec after docker group change
|
||||
DEPLOY_SELF="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")"
|
||||
DEPLOY_QUOTED_ARGS="$(printf '%q ' "$@")"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
@@ -105,12 +109,11 @@ install_docker() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# Add current user to docker group
|
||||
if [ "$os" != "macos" ]; then
|
||||
sudo usermod -aG docker $USER
|
||||
print_warning "You may need to log out and back in for docker group changes to take effect"
|
||||
# Add current user to docker group (activated via re-exec in check_requirements)
|
||||
if [ "$os" != "macos" ] && [ "$(id -u)" -ne 0 ]; then
|
||||
sudo usermod -aG docker "$USER"
|
||||
fi
|
||||
|
||||
|
||||
print_success "Docker installed successfully"
|
||||
}
|
||||
|
||||
@@ -230,13 +233,23 @@ check_requirements() {
|
||||
print_warning "Docker daemon is not running. Starting..."
|
||||
sudo systemctl start docker 2>/dev/null || sudo service docker start 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
# Daemon may be running but the current session lacks the docker group.
|
||||
# `usermod -aG docker` was applied above but only takes effect in a new
|
||||
# login session. Re-exec the entire script under `sg docker` so the
|
||||
# kernel picks up the supplementary GID without a logout/login cycle.
|
||||
if sudo docker info &> /dev/null && \
|
||||
getent group docker | grep -qw "$(id -un)" && \
|
||||
[ -z "$_DEPLOY_REEXEC" ]; then
|
||||
print_info "Activating docker group and restarting deploy..."
|
||||
exec sg docker -c "_DEPLOY_REEXEC=1 bash \"$DEPLOY_SELF\" $DEPLOY_QUOTED_ARGS"
|
||||
fi
|
||||
print_error "Cannot connect to Docker daemon. Please start Docker and try again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
print_success "Docker daemon is running"
|
||||
}
|
||||
|
||||
@@ -512,11 +525,17 @@ check_stale_db_volume() {
|
||||
# configuration. A bad ssl_certificate path makes nginx exit at startup, so
|
||||
# "up -d" returning 0 is not on its own proof that the site is being served.
|
||||
verify_nginx_running() {
|
||||
local max_wait=20
|
||||
local max_wait=60
|
||||
local waited=0
|
||||
|
||||
while [ $waited -lt $max_wait ]; do
|
||||
if docker compose -f compose.yml -f compose.prod.yml ps nginx 2>/dev/null | grep -qi "running\|healthy"; then
|
||||
# Use --format to get the raw container state ("running") instead of
|
||||
# the human-readable STATUS column ("Up Xs (health: starting)") which
|
||||
# does NOT contain the word "running" and only shows "healthy" after
|
||||
# the first healthcheck interval (30s) — longer than the old 20s wait.
|
||||
local state
|
||||
state=$(docker compose -f compose.yml -f compose.prod.yml ps nginx --format '{{.State}}' 2>/dev/null)
|
||||
if [ "$state" = "running" ]; then
|
||||
if docker compose -f compose.yml -f compose.prod.yml exec -T nginx nginx -t >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
@@ -1030,17 +1049,6 @@ install_pve_cli() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
if [ "$mode" = "--auto" ]; then
|
||||
print_warning "Не root — пропускаю авто-установку 'pve' CLI в /usr/local/bin."
|
||||
print_info "Чтобы установить вручную: sudo bash $project_dir/deploy.sh --install-cli"
|
||||
return 0
|
||||
fi
|
||||
print_error "Установка требует root-прав."
|
||||
print_info "Выполните: sudo bash deploy.sh --install-cli"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Идемпотентность: если уже установлен и указывает на ту же директорию — пропускаем
|
||||
if [ "$mode" = "--auto" ] && [ -f "$install_path" ]; then
|
||||
if grep -q "^PVE_DIR=\"$project_dir\"" "$install_path" 2>/dev/null; then
|
||||
@@ -1049,24 +1057,71 @@ install_pve_cli() {
|
||||
fi
|
||||
fi
|
||||
|
||||
print_info "Установка pve CLI..."
|
||||
print_info " Источник : $pve_source"
|
||||
print_info " Установка : $install_path"
|
||||
|
||||
# Подставить реальный путь к директории панели
|
||||
# Prepare the patched script in a temp file
|
||||
local tmp_cli
|
||||
tmp_cli=$(mktemp /tmp/pve-cli-XXXXXX)
|
||||
if ! sed "s|PVE_DIR=\"/opt/pvemanager\"|PVE_DIR=\"$project_dir\"|g" \
|
||||
"$pve_source" > "$install_path"; then
|
||||
print_error "Не удалось записать $install_path"
|
||||
"$pve_source" > "$tmp_cli"; then
|
||||
rm -f "$tmp_cli"
|
||||
print_error "Не удалось подготовить pve CLI"
|
||||
return 1
|
||||
fi
|
||||
chmod +x "$install_path"
|
||||
chmod 755 "$tmp_cli"
|
||||
|
||||
print_success "pve CLI установлен в $install_path"
|
||||
print_info "Теперь вы можете использовать команду 'pve' из любого места."
|
||||
# Try to install to /usr/local/bin (global PATH)
|
||||
local installed=false
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
cp "$tmp_cli" "$install_path" && installed=true
|
||||
else
|
||||
# Not root — try sudo (password may already be cached from docker ops)
|
||||
if sudo cp "$tmp_cli" "$install_path" 2>/dev/null && \
|
||||
sudo chmod 755 "$install_path" 2>/dev/null; then
|
||||
installed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$installed" = true ]; then
|
||||
rm -f "$tmp_cli"
|
||||
print_success "pve CLI установлен в $install_path"
|
||||
print_info "Теперь вы можете использовать команду 'pve' из любого места."
|
||||
if [ "$mode" != "--auto" ]; then
|
||||
echo ""
|
||||
"$install_path" help
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback: install to ~/.local/bin (works without root)
|
||||
local user_bin="${HOME}/.local/bin"
|
||||
local user_install_path="$user_bin/pve"
|
||||
mkdir -p "$user_bin"
|
||||
cp "$tmp_cli" "$user_install_path"
|
||||
rm -f "$tmp_cli"
|
||||
|
||||
print_success "pve CLI установлен в $user_install_path"
|
||||
|
||||
# Ensure ~/.local/bin is in PATH
|
||||
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$user_bin"; then
|
||||
local shell_rc=""
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
shell_rc="$HOME/.bashrc"
|
||||
elif [ -f "$HOME/.profile" ]; then
|
||||
shell_rc="$HOME/.profile"
|
||||
fi
|
||||
|
||||
if [ -n "$shell_rc" ] && ! grep -q '\.local/bin' "$shell_rc" 2>/dev/null; then
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$shell_rc"
|
||||
print_info "Добавлен ~/.local/bin в PATH ($shell_rc)"
|
||||
fi
|
||||
|
||||
export PATH="$user_bin:$PATH"
|
||||
print_warning "Для текущей сессии PATH обновлён. Для новых сессий выполните:"
|
||||
print_info " source $shell_rc"
|
||||
fi
|
||||
|
||||
if [ "$mode" != "--auto" ]; then
|
||||
echo ""
|
||||
"$install_path" help
|
||||
"$user_install_path" help
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
# BuildKit-кеш npm — повторные сборки переиспользуют скачанные пакеты
|
||||
# BuildKit-кеш npm — повторные сборки переиспользуют скачанные пакеты.
|
||||
# npm install вместо npm ci: кросс-платформенный lockfile (Windows-host)
|
||||
# не резолвит Linux-optional deps (@emnapi/core и др.), npm ci падает.
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci --prefer-offline --no-audit --no-fund
|
||||
npm install --prefer-offline --no-audit --no-fund
|
||||
|
||||
# ── Dev stage: Vite HMR (используется compose.dev.yml) ───────────────────────
|
||||
FROM deps AS dev
|
||||
|
||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@fontsource-variable/dm-sans": "^5.2.8",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@novnc/novnc": "^1.6.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
@@ -692,20 +692,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"@emnapi/wasi-threads": "1.2.3",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -713,9 +713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -917,10 +917,10 @@
|
||||
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource-variable/dm-sans": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/dm-sans/-/dm-sans-5.2.8.tgz",
|
||||
"integrity": "sha512-AxkvMTvNWgfrmlyjiV05vlHYJa+nRQCf1EfvIrQAPBpFJW0O9VTz7oAFr9S3lvbWdmnFoBk7yFqQL86u64nl2g==",
|
||||
"node_modules/@fontsource-variable/inter": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
|
||||
"integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@fontsource-variable/dm-sans": "^5.2.8",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@novnc/novnc": "^1.6.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function CommandPalette() {
|
||||
onMouseEnter={() => setActiveIndex(i)}
|
||||
onClick={() => go(item.path)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-[13px] font-medium transition-colors',
|
||||
'flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm font-medium transition-colors',
|
||||
i === activeIndex
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-foreground hover:bg-accent'
|
||||
|
||||
@@ -23,7 +23,7 @@ const buttonVariants = cva(
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
|
||||
@@ -73,7 +73,7 @@ function CircularGauge({ label, value }: { label: string; value: number }) {
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="font-mono text-[13px] font-bold tabular-nums">{Math.round(clamped)}</span>
|
||||
<span className="font-mono text-sm font-bold tabular-nums">{Math.round(clamped)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xs font-mono text-muted-foreground tracking-wide uppercase">{label}</span>
|
||||
|
||||
@@ -1010,7 +1010,7 @@ function BackupDialog({ open, onClose, serverId, vmid, type, node, name }: Omit<
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('backups.storage')}</Label>
|
||||
<Select value={storage} onValueChange={setStorage}>
|
||||
<Select value={storage} onValueChange={(v) => setStorage(v ?? '')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('backups.select_storage')} />
|
||||
</SelectTrigger>
|
||||
@@ -1031,7 +1031,7 @@ function BackupDialog({ open, onClose, serverId, vmid, type, node, name }: Omit<
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('backups.mode')}</Label>
|
||||
<Select value={mode} onValueChange={setMode}>
|
||||
<Select value={mode} onValueChange={(v) => setMode(v ?? '')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="snapshot">Snapshot</SelectItem>
|
||||
@@ -1042,7 +1042,7 @@ function BackupDialog({ open, onClose, serverId, vmid, type, node, name }: Omit<
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('backups.compression')}</Label>
|
||||
<Select value={compress} onValueChange={setCompress}>
|
||||
<Select value={compress} onValueChange={(v) => setCompress(v ?? '')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zstd">ZSTD</SelectItem>
|
||||
|
||||
@@ -35,7 +35,6 @@ export default function ScriptExecuteDialog({ script, onOpenChange }: Props) {
|
||||
// дальше поллим его до завершения (см. useScriptExecution).
|
||||
const { data: liveExec } = useScriptExecution(executionId ?? 0);
|
||||
const result: ScriptExecution | null = liveExec ?? null;
|
||||
const isRunning = result?.status === 'running';
|
||||
|
||||
const serverIdNum = serverId ? Number(serverId) : 0;
|
||||
const { data: nodesData } = useNodes(serverIdNum || 0);
|
||||
@@ -87,7 +86,7 @@ export default function ScriptExecuteDialog({ script, onOpenChange }: Props) {
|
||||
params: paramValues,
|
||||
},
|
||||
{
|
||||
onSuccess: (r) => setResult(r),
|
||||
onSuccess: (r) => setExecutionId(r.id),
|
||||
onError: (e) => toast.error((e as Error).message),
|
||||
},
|
||||
);
|
||||
|
||||
2
frontend/src/fontsource.d.ts
vendored
2
frontend/src/fontsource.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
// @fontsource-variable/* packages ship CSS with no type declarations, so a
|
||||
// bare side-effect import isn't matched by Vite's `*.css` module glob. Declare
|
||||
// them as typeless modules to satisfy `tsc` (see main.tsx font imports).
|
||||
declare module '@fontsource-variable/dm-sans';
|
||||
declare module '@fontsource-variable/inter';
|
||||
declare module '@fontsource-variable/jetbrains-mono';
|
||||
|
||||
@@ -5,14 +5,19 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-sans: "DM Sans Variable", system-ui, sans-serif;
|
||||
--font-sans: "Inter Variable", system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
|
||||
--font-heading: var(--font-sans);
|
||||
|
||||
/* One named step below text-xs (12px) for micro labels / meta text, so the
|
||||
type scale is 11·12·14·16·18·24·32 instead of ad-hoc text-[10px]/[11px]. */
|
||||
/* Micro step for kbd hints, chart axis ticks, and compact badges.
|
||||
Type scale: 11·12·13·14·16·18·24·32 */
|
||||
--text-2xs: 0.6875rem; /* 11px */
|
||||
--text-2xs--line-height: 1rem;
|
||||
/* Bump xs/sm/base up by 1px for better readability at density. */
|
||||
--text-xs: 0.8125rem; /* 13px, default 12px */
|
||||
--text-xs--line-height: 1.125rem;
|
||||
--text-sm: 0.9375rem; /* 15px, default 14px */
|
||||
--text-sm--line-height: 1.375rem;
|
||||
|
||||
/* Semantic status tokens — use text-success / bg-warning / text-danger etc.
|
||||
instead of hardcoded green-500 / amber-500 / red-500 so both themes and
|
||||
|
||||
@@ -76,7 +76,7 @@ function SidebarContent() {
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-[13px] font-medium transition-colors ${
|
||||
className={`flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
@@ -118,7 +118,7 @@ function UserSection() {
|
||||
{user?.username?.[0]?.toUpperCase() ?? '?'}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[13px] font-medium">{user?.username ?? 'User'}</p>
|
||||
<p className="truncate text-sm font-medium">{user?.username ?? 'User'}</p>
|
||||
<p className="truncate text-2xs text-muted-foreground">
|
||||
{user?.is_admin ? 'Admin' : 'User'}
|
||||
</p>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
// Self-hosted fonts (font-display: swap built in) — no render-blocking
|
||||
// external Google Fonts request. Family names: "DM Sans Variable" /
|
||||
// external Google Fonts request. Family names: "Inter Variable" /
|
||||
// "JetBrains Mono Variable" (see --font-sans / --font-mono in index.css).
|
||||
import '@fontsource-variable/dm-sans'
|
||||
import '@fontsource-variable/inter'
|
||||
import '@fontsource-variable/jetbrains-mono'
|
||||
import './lib/i18n'
|
||||
import './index.css'
|
||||
|
||||
Reference in New Issue
Block a user