10 Commits

Author SHA1 Message Date
0220f338db docs: add CONTRIBUTING and SECURITY links to Russian README
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
2026-08-17 02:00:57 +05:00
985f04c7ad docs: add CONTRIBUTING and SECURITY links to README
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
2026-08-17 01:59:39 +05:00
8b3cde79cf docs: replace template SECURITY.md with project-specific policy
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-17 01:56:57 +05:00
72d933228c docs: add contributing guide for new contributors
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
2026-08-17 01:52:18 +05:00
5999183c8e fix: activate docker group in current session via sg re-exec
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
After install_docker adds the user to the docker group, the change only
takes effect in a new login session.  The script continued running in
the old session, docker info failed, and deploy exited with "Cannot
connect to Docker daemon" even though the daemon was running fine.

Now check_requirements detects this situation (sudo docker works but
docker doesn't, and the user IS in the docker group per /etc/group)
and re-execs the entire script under `sg docker` to activate the
supplementary GID without a logout/login cycle.  A _DEPLOY_REEXEC
guard prevents infinite loops.
2026-08-17 01:42:05 +05:00
286576715b docs: release notes and version bump for v1.16.1
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
2026-08-17 01:26:05 +05:00
820da8ca01 fix: set 755 permissions on pve CLI so non-root users can read the script
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
sudo cp preserves the source umask which can leave the file without
read permission for group/others.  A bash script needs both read and
execute bits — without read, the shell cannot interpret it.
2026-08-17 01:20:43 +05:00
a275b8bed5 fix: prevent SSL rollback by using container state instead of status text
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
verify_nginx_running grepped for "running|healthy" in the human-readable
STATUS column of docker compose ps, but the column shows "Up Xs (health:
starting)" for the first 30s before the healthcheck passes — neither
word matches.  With max_wait=20s the function always timed out and the
deploy script rolled back to the HTTP-only config even though the SSL
certificate was successfully obtained.

Switch to --format '{{.State}}' which returns the raw "running" state
immediately, and bump max_wait to 60s for slow first starts.
2026-08-17 01:13:43 +05:00
fbfbd76960 fix: install pve CLI without root via sudo fallback to ~/.local/bin
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
Previously install_pve_cli silently skipped installation when deploy.sh
was run without root, leaving the 'pve' command unavailable.  Now it
tries sudo first (password is usually cached from docker operations)
and falls back to ~/.local/bin with automatic PATH configuration.
2026-08-17 00:34:58 +05:00
8fa1208cde feat(frontend): switch to Inter Variable, bump type scale, fix TS errors
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
- Replace DM Sans with Inter Variable (better screen rendering at small sizes)
- Bump text-xs 12→13px, text-sm 14→15px globally via theme tokens
- Fix hardcoded text-[13px] → text-sm in AppLayout, CommandPalette, DashboardPage
- Fix button sm variant text-[0.8rem] → text-xs for consistency
- Fix TS: ScriptExecuteDialog unused isRunning, setResult → setExecutionId(r.id)
- Fix TS: InstanceActionDialogs onValueChange null guards (string | null → string)
- Dockerfile: npm ci → npm install (cross-platform lockfile, Linux optional deps)
2026-08-17 00:14:06 +05:00
20 changed files with 397 additions and 92 deletions

View File

@@ -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

View File

@@ -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
View 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).

View File

@@ -4,7 +4,7 @@
**Modern web panel for managing Proxmox servers, virtual machines and LXC containers**
[![Version](https://img.shields.io/badge/version-1.16.0-blue?style=flat-square)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.16.1-blue?style=flat-square)](CHANGELOG.md)
[![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE)
[![Docker](https://img.shields.io/badge/docker-ready-2496ED?style=flat-square&logo=docker&logoColor=white)](compose.yml)
[![Python](https://img.shields.io/badge/python-3.12-3776AB?style=flat-square&logo=python&logoColor=white)](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>

View File

@@ -4,7 +4,7 @@
**Современная веб-панель для управления серверами Proxmox, виртуальными машинами и контейнерами LXC**
[![Версия](https://img.shields.io/badge/версия-1.16.0-blue?style=flat-square)](CHANGELOG.md)
[![Версия](https://img.shields.io/badge/версия-1.16.1-blue?style=flat-square)](CHANGELOG.md)
[![Лицензия](https://img.shields.io/badge/лицензия-MIT-green?style=flat-square)](LICENSE)
[![Docker](https://img.shields.io/badge/docker-готов-2496ED?style=flat-square&logo=docker&logoColor=white)](compose.yml)
[![Python](https://img.shields.io/badge/python-3.12-3776AB?style=flat-square&logo=python&logoColor=white)](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>

View File

@@ -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.

View File

@@ -1 +1 @@
1.16.0
1.16.1

117
deploy.sh
View File

@@ -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
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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",

View File

@@ -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'

View File

@@ -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":

View File

@@ -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>

View File

@@ -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>

View File

@@ -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),
},
);

View File

@@ -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';

View File

@@ -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

View File

@@ -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>

View File

@@ -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'