From 24f8c4d51850d5f23cba6f1b4c94236a9761eafd Mon Sep 17 00:00:00 2001 From: alekan Date: Mon, 16 Feb 2026 23:02:46 +0300 Subject: [PATCH 01/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D1=83=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=B5=D0=BF=D0=BE=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/admin/admin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js index 425f28a..a373aa1 100644 --- a/frontend/admin/admin.js +++ b/frontend/admin/admin.js @@ -129,6 +129,7 @@ ${escapeHtml(u.username)} ${ROLE_LABELS[u.role] || u.role} + `).join(''); } From 64d85eab55671ceca1c004d66e4a80a47f3400ff Mon Sep 17 00:00:00 2001 From: EgorZuev <вÐyegorzuev@gmail.com> Date: Wed, 18 Feb 2026 21:56:57 +0000 Subject: [PATCH 02/14] docs(agent): add rules and skills --- .agent/rules/main.md | 46 +++++++++++++++++++ .agent/skills/git-commit-formatter/SKILL.md | 30 ++++++++++++ .agent/skills/git-push/SKILL.md | 41 +++++++++++++++++ .../skills/git-push/scripts/push_changes.sh | 31 +++++++++++++ .agent/skills/git-sync/SKILL.md | 23 ++++++++++ .../skills/git-sync/scripts/check_and_pull.sh | 43 +++++++++++++++++ .gitignore | 7 ++- 7 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 .agent/rules/main.md create mode 100644 .agent/skills/git-commit-formatter/SKILL.md create mode 100644 .agent/skills/git-push/SKILL.md create mode 100755 .agent/skills/git-push/scripts/push_changes.sh create mode 100644 .agent/skills/git-sync/SKILL.md create mode 100644 .agent/skills/git-sync/scripts/check_and_pull.sh diff --git a/.agent/rules/main.md b/.agent/rules/main.md new file mode 100644 index 0000000..7054edf --- /dev/null +++ b/.agent/rules/main.md @@ -0,0 +1,46 @@ +--- +trigger: always_on +--- + +# Project Context: University Schedule System + +## Project Overview +This project is a university scheduling system website. +- **Role**: Educational platform for managing schedules. +- **Language**: Mixed (Java Backend + Web Frontend). +- **Public URL**: https://magistr.zuev.company + +## Directory Structure & Responsibilities +The project follows a specific folder structure. You must adhere to these paths: + +- **`backend/`**: Contains the **Java** backend application source code. + - When working on API or server logic, focus here. + +- **`frontend/`**: Contains the frontend source code. Note the strict role separation: + - `frontend/admin/`: Code specific to the **Administrator** interface. + - `frontend/teacher/`: Code specific to the **Teacher** interface. + - `frontend/student/`: Code specific to the **Student** interface. + - *Constraint*: Do not mix logic between these folders unless creating a shared utility. + +- **`db/`**: Database configuration and data. + - `db/init/init.sql`: The SQL script responsible for **creating and initializing** the database schema (tables, initial data). + +- **Root Files**: + - `compose.yaml`: The Docker Compose configuration. This file defines the services (backend, db, frontend servers) and how they run together. + - `.env`: Environment variables. Contains sensitive config (DB passwords, ports, API keys). + +## External Dependencies (Parent Directory) +Some infrastructure components are located outside the project root: + +- **`../caddy-proxy/`**: Located one level up relative to the project root. + - **Role**: Reverse proxy handling traffic for `magistr.zuev.company`. + - **`Caddyfile`**: Configuration for routing and SSL. + - **`compose.yaml`**: A separate Docker Compose file specifically for the proxy service. + +## Workflow Guidelines +1. **Database Changes**: If you need to modify the database schema, you must update `db/init/init.sql` so the changes persist when the container is rebuilt. +2. **Configuration**: If adding new configuration parameters, add them to `.env` and reference them in `compose.yaml` or the application code. +3. **Routing/Proxy**: If there are issues with the domain or external access, check the configuration in `../caddy-proxy/Caddyfile`. + +## Language Preference +- **Always answer in Russian**: This is a strict requirement from the user. All explanations, comments, and interactions must be in Russian unless specifically asked otherwise. \ No newline at end of file diff --git a/.agent/skills/git-commit-formatter/SKILL.md b/.agent/skills/git-commit-formatter/SKILL.md new file mode 100644 index 0000000..bc24790 --- /dev/null +++ b/.agent/skills/git-commit-formatter/SKILL.md @@ -0,0 +1,30 @@ +--- +name: git-commit-formatter +description: Formats git commit messages according to Conventional Commits specification. Use this when the user asks to commit changes or write a commit message. +--- + +# Git Commit Formatter Skill + +When writing a git commit message, you MUST follow the Conventional Commits specification. + +## Format +`[optional scope]: ` + +## Allowed Types +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing tests or correcting existing tests +- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation + +## Instructions +1. Analyze the changes to determine the primary `type`. +2. Identify the `scope` if applicable (e.g., specific component or file). +3. Write a concise `description` in imperative mood (e.g., "add feature" not "added feature"). +4. If there are breaking changes, add a footer starting with `BREAKING CHANGE:`. + +## Example +`feat(auth): implement login with google` \ No newline at end of file diff --git a/.agent/skills/git-push/SKILL.md b/.agent/skills/git-push/SKILL.md new file mode 100644 index 0000000..5c88c3d --- /dev/null +++ b/.agent/skills/git-push/SKILL.md @@ -0,0 +1,41 @@ +# Git Push & Format Skill + +## Description +Formats git commit messages according to Conventional Commits specification and pushes changes to the remote repository. Use this when the user asks to commit changes, save progress, or upload code. + +## Triggers +- "Запушь изменения" (Push changes) +- "Сделай коммит" (Make a commit) +- "Сохрани в гит" (Save to git) +- "Сделай пуш" (Make a push) +- "Запушь" (Push) + +## Format (Conventional Commits) +When writing a git commit message, you MUST follow this format: +`[optional scope]: ` + +### Allowed Types +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing tests or correcting existing tests +- **chore**: Changes to the build process or auxiliary tools and libraries + +### Instructions for Agent +1. Analyze the user's request or recent file changes to determine the `type` and `description`. +2. Construct the commit message string (e.g., "fix(auth): correct token validation"). +3. **Execute** the bash script below, passing the generated message as an argument. + +## Execution +Run the following command (replace .YOUR_MESSAGE. with the formatted string): + +```bash +/bin/bash .agent/skills/git-push/scripts/push_changes.sh "YOUR_MESSAGE" +``` + +## Example Usage +If user says "I fixed the login bug", you execute: +`/bin/bash .agent/skills/git-push/scripts/push_changes.sh "fix(auth): resolve login error"` \ No newline at end of file diff --git a/.agent/skills/git-push/scripts/push_changes.sh b/.agent/skills/git-push/scripts/push_changes.sh new file mode 100755 index 0000000..21409e9 --- /dev/null +++ b/.agent/skills/git-push/scripts/push_changes.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# --- НАСТРОЙКИ (Проверь, чтобы совпадали с check_and_pull.sh) --- +SERVER="root@192.168.1.87" +REMOTE_PATH="/root/magistr/program" +# ------------------------------------------------------------- + +COMMIT_MSG="$1" + +# Проверка: если сообщение пустое, ругаемся +if [ -z "$COMMIT_MSG" ]; then + echo "❌ Error: Commit message is required." + echo "Usage: ./push_changes.sh \"feat: description\"" + exit 1 +fi + +echo "🚀 Starting Push Sequence to $SERVER..." + +# Выполняем цепочку команд на сервере одной строкой +ssh -o BatchMode=yes "$SERVER" "export LANG=C.UTF-8 && cd $REMOTE_PATH && \ + git add . && \ + git commit -m \"$COMMIT_MSG\" && \ + git push origin main" + +# Проверяем код возврата последней команды +if [ $? -eq 0 ]; then + echo "✅ Success! Changes pushed to remote." +else + echo "❌ Failed to push. Check the output above for errors." + exit 1 +fi \ No newline at end of file diff --git a/.agent/skills/git-sync/SKILL.md b/.agent/skills/git-sync/SKILL.md new file mode 100644 index 0000000..7d6263e --- /dev/null +++ b/.agent/skills/git-sync/SKILL.md @@ -0,0 +1,23 @@ +# Git Sync Skill + +## Description +This skill allows the agent to check the remote Git repository for updates. It automatically pulls changes if a new version is available on the remote server. + +## Triggers +Activate this skill when the user asks: +- "Проверь обновления" (Check for updates) +- "Загрузи новую версию" (Download new version) +- "Синхронизируй с гитом" (Sync with git) +- "Есть ли изменения?" (Are there changes?) + +## Execution +To run this skill, execute the bash script: + +```bash +/bin/bash .agent/skills/git-sync/scripts/check_and_pull.sh +``` + +## Response Guidelines +1. **Success**: If the script says "Successfully updated", inform the user the project is now on the latest version. +2. **No Updates**: If the script says "Up-to-date", tell the user no changes were found. +3. **Error**: If the script fails or reports conflicts, ask the user to check git status manually. \ No newline at end of file diff --git a/.agent/skills/git-sync/scripts/check_and_pull.sh b/.agent/skills/git-sync/scripts/check_and_pull.sh new file mode 100644 index 0000000..71e8152 --- /dev/null +++ b/.agent/skills/git-sync/scripts/check_and_pull.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# --- НАСТРОЙКИ --- +SERVER="root@192.168.1.87" +REMOTE_PATH="/root/magistr/program" +# ----------------- + +echo "📡 Connecting to remote server ($SERVER)..." + +# 1. Запускаем fetch прямо на сервере +# Флаг -o BatchMode=yes запрещает спрашивать пароль (чтобы скрипт не завис) +ssh -o BatchMode=yes -o ConnectTimeout=10 "$SERVER" "cd $REMOTE_PATH && git fetch origin" + +if [ $? -ne 0 ]; then + echo "❌ Connection failed." + echo "Make sure SSH keys are set up and the server is reachable." + exit 1 +fi + +# 2. Проверяем количество новых коммитов (HEAD..@{u}) +BEHIND_COUNT=$(ssh "$SERVER" "cd $REMOTE_PATH && git rev-list --count HEAD..@{u} 2>/dev/null") + +# Если переменная пустая — значит ошибка в гите +if [ -z "$BEHIND_COUNT" ]; then + echo "⚠️ Git status unknown. Upstream branch might not be set." + exit 1 +fi + +# 3. Логика обновления +if [ "$BEHIND_COUNT" -gt 0 ]; then + echo "⬇️ Found $BEHIND_COUNT new commit(s). Pulling on server..." + + # Выполняем git pull на сервере + ssh "$SERVER" "cd $REMOTE_PATH && git pull" + + if [ $? -eq 0 ]; then + echo "✅ Server successfully updated!" + else + echo "❌ Update failed (merge conflicts?)." + fi +else + echo "✨ Server is already up to date." +fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5922140..fdc5283 100755 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,16 @@ # Игнорируем данные БД (но не init-скрипты) db/data/ -postgres_data/ # Игнорируем секреты .env -GEMINI.md -AGENTS.md +!GEMINI.md +!AGENTS.md # Игнорируем системные папки IDE (если редактируете с ПК) .idea/ .vscode/ *.DS_Store -.agent +!.agent # Игнорируем временные файлы сборки (на будущее) backend/target/ From ed8668c599725e5bb9066f00990811b197381e08 Mon Sep 17 00:00:00 2001 From: Zuev Date: Thu, 19 Feb 2026 20:33:47 +0300 Subject: [PATCH 03/14] chore: update agent rules, skills and workflows --- .agent/rules/main.md | 64 +++++++++---------- .agent/skills/git-commit-formatter/SKILL.md | 38 +++++------ .agent/skills/git-push/SKILL.md | 56 ++++++++-------- .../skills/git-push/scripts/push_changes.sh | 18 ++---- .agent/skills/git-sync/SKILL.md | 30 ++++----- .../skills/git-sync/scripts/check_and_pull.sh | 27 +++----- .agent/workflows/deploy-server.md | 13 ++++ 7 files changed, 123 insertions(+), 123 deletions(-) create mode 100644 .agent/workflows/deploy-server.md diff --git a/.agent/rules/main.md b/.agent/rules/main.md index 7054edf..e27a4c3 100644 --- a/.agent/rules/main.md +++ b/.agent/rules/main.md @@ -2,45 +2,45 @@ trigger: always_on --- -# Project Context: University Schedule System +# Контекст проекта: Система расписания университета -## Project Overview -This project is a university scheduling system website. -- **Role**: Educational platform for managing schedules. -- **Language**: Mixed (Java Backend + Web Frontend). -- **Public URL**: https://magistr.zuev.company +## Обзор проекта +Этот проект представляет собой веб-сайт системы управления университетским расписанием. +- **Роль**: Образовательная платформа для управления расписанием. +- **Язык**: Смешанный (Java Backend + Web Frontend). +- **Публичный URL**: https://magistr.zuev.company -## Directory Structure & Responsibilities -The project follows a specific folder structure. You must adhere to these paths: +## Структура директорий и обязанности +Проект следует определенной структуре папок. Вы должны придерживаться этих путей: -- **`backend/`**: Contains the **Java** backend application source code. - - When working on API or server logic, focus here. +- **`backend/`**: Содержит исходный код бэкенд-приложения на **Java**. + - При работе с API или серверной логикой фокусируйтесь здесь. -- **`frontend/`**: Contains the frontend source code. Note the strict role separation: - - `frontend/admin/`: Code specific to the **Administrator** interface. - - `frontend/teacher/`: Code specific to the **Teacher** interface. - - `frontend/student/`: Code specific to the **Student** interface. - - *Constraint*: Do not mix logic between these folders unless creating a shared utility. +- **`frontend/`**: Содержит исходный код фронтенда. Обратите внимание на строгое разделение ролей: + - `frontend/admin/`: Код, специфичный для интерфейса **Администратора**. + - `frontend/teacher/`: Код, специфичный для интерфейса **Преподавателя**. + - `frontend/student/`: Код, специфичный для интерфейса **Студента**. + - *Ограничение*: Не смешивайте логику между этими папками, если только не создаете общую утилиту. -- **`db/`**: Database configuration and data. - - `db/init/init.sql`: The SQL script responsible for **creating and initializing** the database schema (tables, initial data). +- **`db/`**: Конфигурация и данные базы данных. + - `db/init/init.sql`: SQL-скрипт, отвечающий за **создание и инициализацию** схемы базы данных (таблицы, начальные данные). -- **Root Files**: - - `compose.yaml`: The Docker Compose configuration. This file defines the services (backend, db, frontend servers) and how they run together. - - `.env`: Environment variables. Contains sensitive config (DB passwords, ports, API keys). +- **Корневые файлы**: + - `compose.yaml`: Конфигурация Docker Compose. Этот файл определяет сервисы (бэкенд, БД, фронтенд-серверы) и то, как они работают вместе. + - `.env`: Переменные окружения. Содержит конфиденциальные настройки (пароли БД, порты, ключи API). -## External Dependencies (Parent Directory) -Some infrastructure components are located outside the project root: +## Внешние зависимости (родительская директория) +Некоторые компоненты инфраструктуры расположены за пределами корня проекта: -- **`../caddy-proxy/`**: Located one level up relative to the project root. - - **Role**: Reverse proxy handling traffic for `magistr.zuev.company`. - - **`Caddyfile`**: Configuration for routing and SSL. - - **`compose.yaml`**: A separate Docker Compose file specifically for the proxy service. +- **`../caddy-proxy/`**: Находится на один уровень выше относительно корня проекта. + - **Роль**: Реверс-прокси, обрабатывающий трафик для `magistr.zuev.company`. + - **`Caddyfile`**: Конфигурация для маршрутизации и SSL. + - **`compose.yaml`**: Отдельный файл Docker Compose специально для службы прокси. -## Workflow Guidelines -1. **Database Changes**: If you need to modify the database schema, you must update `db/init/init.sql` so the changes persist when the container is rebuilt. -2. **Configuration**: If adding new configuration parameters, add them to `.env` and reference them in `compose.yaml` or the application code. -3. **Routing/Proxy**: If there are issues with the domain or external access, check the configuration in `../caddy-proxy/Caddyfile`. +## Рекомендации по рабочему процессу +1. **Изменения в базе данных**: Если вам нужно изменить схему базы данных, вы должны обновить `db/init/init.sql`, чтобы изменения сохранялись при пересборке контейнера. +2. **Конфигурация**: Если вы добавляете новые параметры конфигурации, добавьте их в `.env` и сошлитесь на них в `compose.yaml` или коде приложения. +3. **Маршрутизация/Прокси**: Если возникают проблемы с доменом или внешним доступом, проверьте конфигурацию в `../caddy-proxy/Caddyfile`. -## Language Preference -- **Always answer in Russian**: This is a strict requirement from the user. All explanations, comments, and interactions must be in Russian unless specifically asked otherwise. \ No newline at end of file +## Языковые предпочтения +- **Всегда отвечайте на русском**: Это строгое требование пользователя. Все объяснения, комментарии и взаимодействия должны быть на русском языке, если только не будет специально запрошено иное. \ No newline at end of file diff --git a/.agent/skills/git-commit-formatter/SKILL.md b/.agent/skills/git-commit-formatter/SKILL.md index bc24790..d57aa1d 100644 --- a/.agent/skills/git-commit-formatter/SKILL.md +++ b/.agent/skills/git-commit-formatter/SKILL.md @@ -1,30 +1,30 @@ --- name: git-commit-formatter -description: Formats git commit messages according to Conventional Commits specification. Use this when the user asks to commit changes or write a commit message. +description: Форматирует сообщения коммитов git в соответствии со спецификацией Conventional Commits. Используйте этот навык, когда пользователь просит закоммитить изменения или написать сообщение к коммиту. --- -# Git Commit Formatter Skill +# Навык форматирования коммитов Git -When writing a git commit message, you MUST follow the Conventional Commits specification. +При написании сообщения коммита вы ДОЛЖНЫ следовать спецификации Conventional Commits. -## Format +## Формат `[optional scope]: ` -## Allowed Types -- **feat**: A new feature -- **fix**: A bug fix -- **docs**: Documentation only changes -- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) -- **refactor**: A code change that neither fixes a bug nor adds a feature -- **perf**: A code change that improves performance -- **test**: Adding missing tests or correcting existing tests -- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation +## Допустимые типы +- **feat**: Новая функциональность +- **fix**: Исправление ошибки +- **docs**: Изменения только в документации +- **style**: Изменения, не влияющие на смысл кода (пробелы, форматирование и т.д.) +- **refactor**: Изменение кода, которое не исправляет ошибку и не добавляет функциональность +- **perf**: Изменение кода, повышающее производительность +- **test**: Добавление недостающих тестов или исправление существующих +- **chore**: Изменения в процессе сборки или вспомогательных инструментах и библиотеках -## Instructions -1. Analyze the changes to determine the primary `type`. -2. Identify the `scope` if applicable (e.g., specific component or file). -3. Write a concise `description` in imperative mood (e.g., "add feature" not "added feature"). -4. If there are breaking changes, add a footer starting with `BREAKING CHANGE:`. +## Инструкции +1. Проанализируйте изменения, чтобы определить основной тип (`type`). +2. Определите область (`scope`), если это применимо (например, конкретный компонент или файл). +3. Напишите краткое описание (`description`) в повелительном наклонении (например, "add feature", а не "added feature"). +4. Если есть критические изменения, добавьте подвал, начинающийся с `BREAKING CHANGE:`. -## Example +## Пример `feat(auth): implement login with google` \ No newline at end of file diff --git a/.agent/skills/git-push/SKILL.md b/.agent/skills/git-push/SKILL.md index 5c88c3d..db9b84f 100644 --- a/.agent/skills/git-push/SKILL.md +++ b/.agent/skills/git-push/SKILL.md @@ -1,41 +1,41 @@ -# Git Push & Format Skill +# Навык Git Push и форматирования -## Description -Formats git commit messages according to Conventional Commits specification and pushes changes to the remote repository. Use this when the user asks to commit changes, save progress, or upload code. +## Описание +Форматирует сообщения коммитов git в соответствии со спецификацией Conventional Commits и отправляет изменения в удаленный репозиторий. Используйте этот навык, когда пользователь просит закоммитить изменения, сохранить прогресс или отправить код. -## Triggers -- "Запушь изменения" (Push changes) -- "Сделай коммит" (Make a commit) -- "Сохрани в гит" (Save to git) -- "Сделай пуш" (Make a push) -- "Запушь" (Push) +## Триггеры +- "Запушь изменения" +- "Сделай коммит" +- "Сохрани в гит" +- "Сделай пуш" +- "Запушь" -## Format (Conventional Commits) -When writing a git commit message, you MUST follow this format: +## Формат (Conventional Commits) +При написании сообщения коммита вы ДОЛЖНЫ следовать этому формату: `[optional scope]: ` -### Allowed Types -- **feat**: A new feature -- **fix**: A bug fix -- **docs**: Documentation only changes -- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) -- **refactor**: A code change that neither fixes a bug nor adds a feature -- **perf**: A code change that improves performance -- **test**: Adding missing tests or correcting existing tests -- **chore**: Changes to the build process or auxiliary tools and libraries +### Допустимые типы +- **feat**: Новая функциональность +- **fix**: Исправление ошибки +- **docs**: Изменения только в документации +- **style**: Изменения, не влияющие на смысл кода (пробелы, форматирование и т.д.) +- **refactor**: Изменение кода, которое не исправляет ошибку и не добавляет функциональность +- **perf**: Изменение кода, повышающее производительность +- **test**: Добавление недостающих тестов или исправление существующих +- **chore**: Изменения в процессе сборки или вспомогательных инструментах и библиотеках -### Instructions for Agent -1. Analyze the user's request or recent file changes to determine the `type` and `description`. -2. Construct the commit message string (e.g., "fix(auth): correct token validation"). -3. **Execute** the bash script below, passing the generated message as an argument. +### Инструкции для агента +1. Проанализируйте запрос пользователя или недавние изменения файлов, чтобы определить тип (`type`) и описание (`description`). +2. Сформируйте строку сообщения коммита (например, "fix(auth): correct token validation"). +3. **Выполните** bash-скрипт ниже, передав сгенерированное сообщение в качестве аргумента. -## Execution -Run the following command (replace .YOUR_MESSAGE. with the formatted string): +## Выполнение +Запустите следующую команду (замените "YOUR_MESSAGE" на отформатированную строку): ```bash /bin/bash .agent/skills/git-push/scripts/push_changes.sh "YOUR_MESSAGE" ``` -## Example Usage -If user says "I fixed the login bug", you execute: +## Пример использования +Если пользователь говорит "Я исправил ошибку входа", вы выполняете: `/bin/bash .agent/skills/git-push/scripts/push_changes.sh "fix(auth): resolve login error"` \ No newline at end of file diff --git a/.agent/skills/git-push/scripts/push_changes.sh b/.agent/skills/git-push/scripts/push_changes.sh index 21409e9..d8cdc9f 100755 --- a/.agent/skills/git-push/scripts/push_changes.sh +++ b/.agent/skills/git-push/scripts/push_changes.sh @@ -1,10 +1,5 @@ #!/bin/bash -# --- НАСТРОЙКИ (Проверь, чтобы совпадали с check_and_pull.sh) --- -SERVER="root@192.168.1.87" -REMOTE_PATH="/root/magistr/program" -# ------------------------------------------------------------- - COMMIT_MSG="$1" # Проверка: если сообщение пустое, ругаемся @@ -14,15 +9,14 @@ if [ -z "$COMMIT_MSG" ]; then exit 1 fi -echo "🚀 Starting Push Sequence to $SERVER..." +echo "🚀 Starting Local Push Sequence..." -# Выполняем цепочку команд на сервере одной строкой -ssh -o BatchMode=yes "$SERVER" "export LANG=C.UTF-8 && cd $REMOTE_PATH && \ - git add . && \ - git commit -m \"$COMMIT_MSG\" && \ - git push origin main" +# Выполняем цепочку команд локально +git add . && \ +git commit -m "$COMMIT_MSG" && \ +git push origin main -# Проверяем код возврата последней команды +# Проверяем код возврата if [ $? -eq 0 ]; then echo "✅ Success! Changes pushed to remote." else diff --git a/.agent/skills/git-sync/SKILL.md b/.agent/skills/git-sync/SKILL.md index 7d6263e..9848bfe 100644 --- a/.agent/skills/git-sync/SKILL.md +++ b/.agent/skills/git-sync/SKILL.md @@ -1,23 +1,23 @@ -# Git Sync Skill +# Навык Git Sync -## Description -This skill allows the agent to check the remote Git repository for updates. It automatically pulls changes if a new version is available on the remote server. +## Описание +Этот навык позволяет агенту проверять удаленный Git-репозиторий на наличие обновлений. Он автоматически подтягивает изменения, если на удаленном сервере доступна новая версия. -## Triggers -Activate this skill when the user asks: -- "Проверь обновления" (Check for updates) -- "Загрузи новую версию" (Download new version) -- "Синхронизируй с гитом" (Sync with git) -- "Есть ли изменения?" (Are there changes?) +## Триггеры +Активируйте этот навык, когда пользователь спрашивает: +- "Проверь обновления" +- "Загрузи новую версию" +- "Синхронизируй с гитом" +- "Есть ли изменения?" -## Execution -To run this skill, execute the bash script: +## Выполнение +Для запуска этого навыка выполните bash-скрипт: ```bash /bin/bash .agent/skills/git-sync/scripts/check_and_pull.sh ``` -## Response Guidelines -1. **Success**: If the script says "Successfully updated", inform the user the project is now on the latest version. -2. **No Updates**: If the script says "Up-to-date", tell the user no changes were found. -3. **Error**: If the script fails or reports conflicts, ask the user to check git status manually. \ No newline at end of file +## Рекомендации по ответам +1. **Успех**: Если скрипт говорит "Successfully updated", сообщите пользователю, что проект обновлен до последней версии. +2. **Нет обновлений**: Если скрипт говорит "Up-to-date", сообщите пользователю, что изменений не найдено. +3. **Ошибка**: Если скрипт завершился с ошибкой или сообщает о конфликтах, попросите пользователя проверить статус git вручную. \ No newline at end of file diff --git a/.agent/skills/git-sync/scripts/check_and_pull.sh b/.agent/skills/git-sync/scripts/check_and_pull.sh index 71e8152..785cd89 100644 --- a/.agent/skills/git-sync/scripts/check_and_pull.sh +++ b/.agent/skills/git-sync/scripts/check_and_pull.sh @@ -1,24 +1,17 @@ #!/bin/bash -# --- НАСТРОЙКИ --- -SERVER="root@192.168.1.87" -REMOTE_PATH="/root/magistr/program" -# ----------------- +echo "📡 Checking for updates locally..." -echo "📡 Connecting to remote server ($SERVER)..." - -# 1. Запускаем fetch прямо на сервере -# Флаг -o BatchMode=yes запрещает спрашивать пароль (чтобы скрипт не завис) -ssh -o BatchMode=yes -o ConnectTimeout=10 "$SERVER" "cd $REMOTE_PATH && git fetch origin" +# 1. Запускаем fetch локально +git fetch origin if [ $? -ne 0 ]; then - echo "❌ Connection failed." - echo "Make sure SSH keys are set up and the server is reachable." + echo "❌ Fetch failed. Check your internet connection and git remote settings." exit 1 fi # 2. Проверяем количество новых коммитов (HEAD..@{u}) -BEHIND_COUNT=$(ssh "$SERVER" "cd $REMOTE_PATH && git rev-list --count HEAD..@{u} 2>/dev/null") +BEHIND_COUNT=$(git rev-list --count HEAD..@{u} 2>/dev/null) # Если переменная пустая — значит ошибка в гите if [ -z "$BEHIND_COUNT" ]; then @@ -28,16 +21,16 @@ fi # 3. Логика обновления if [ "$BEHIND_COUNT" -gt 0 ]; then - echo "⬇️ Found $BEHIND_COUNT new commit(s). Pulling on server..." + echo "⬇️ Found $BEHIND_COUNT new commit(s). Pulling changes..." - # Выполняем git pull на сервере - ssh "$SERVER" "cd $REMOTE_PATH && git pull" + # Выполняем git pull + git pull if [ $? -eq 0 ]; then - echo "✅ Server successfully updated!" + echo "✅ Successfully updated!" else echo "❌ Update failed (merge conflicts?)." fi else - echo "✨ Server is already up to date." + echo "✨ Already up to date." fi \ No newline at end of file diff --git a/.agent/workflows/deploy-server.md b/.agent/workflows/deploy-server.md new file mode 100644 index 0000000..08d425b --- /dev/null +++ b/.agent/workflows/deploy-server.md @@ -0,0 +1,13 @@ +--- +description: Деплой на удаленный сервер 192.168.1.87 (Git Pull + Docker Build) +--- + +# Развертывание на сервере 192.168.1.87 + +Этот воркфлоу позволяет быстро обновить проект на удаленном сервере после того, как вы запушили изменения в Git. + +// turbo-all +1. Синхронизировать код и перезапустить контейнеры: +```bash +ssh root@192.168.1.87 "cd /root/magistr/program/ && git fetch origin main && git reset --hard origin/main && docker compose up -d --build" +``` From e9c08b4c7529131cce897b4ee6b4e07770f302e7 Mon Sep 17 00:00:00 2001 From: Zuev Date: Thu, 19 Feb 2026 20:42:25 +0300 Subject: [PATCH 04/14] chore(git): update git-push skill and script with SSH info --- .agent/skills/git-push/SKILL.md | 9 ++++++--- .agent/skills/git-push/scripts/push_changes.sh | 8 +++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.agent/skills/git-push/SKILL.md b/.agent/skills/git-push/SKILL.md index db9b84f..14427a5 100644 --- a/.agent/skills/git-push/SKILL.md +++ b/.agent/skills/git-push/SKILL.md @@ -1,7 +1,7 @@ # Навык Git Push и форматирования ## Описание -Форматирует сообщения коммитов git в соответствии со спецификацией Conventional Commits и отправляет изменения в удаленный репозиторий. Используйте этот навык, когда пользователь просит закоммитить изменения, сохранить прогресс или отправить код. +Форматирует сообщения коммитов git в соответствии со спецификацией Conventional Commits и отправляет изменения в удаленный репозиторий. Использует SSH URL: `ssh://git@gitea.zuev.company:2222/Zuev/magistr.git`. Используйте этот навык, когда пользователь просит закоммитить изменения, сохранить прогресс или отправить код. ## Триггеры - "Запушь изменения" @@ -9,6 +9,9 @@ - "Сохрани в гит" - "Сделай пуш" - "Запушь" +- "Пуш" +- "Отправь код" +- "Commit and push" ## Формат (Conventional Commits) При написании сообщения коммита вы ДОЛЖНЫ следовать этому формату: @@ -25,9 +28,9 @@ - **chore**: Изменения в процессе сборки или вспомогательных инструментах и библиотеках ### Инструкции для агента -1. Проанализируйте запрос пользователя или недавние изменения файлов, чтобы определить тип (`type`) и описание (`description`). +1. Проанализируйте запрос пользователя или недавние изменения файлов, чтобы определить тип (`type`) и область (`scope`), если применимо. 2. Сформируйте строку сообщения коммита (например, "fix(auth): correct token validation"). -3. **Выполните** bash-скрипт ниже, передав сгенерированное сообщение в качестве аргумента. +3. **ОБЯЗАТЕЛЬНО** выполните bash-скрипт ниже, передав сгенерированное сообщение в качестве аргумента. Не пытайтесь выполнять git команды вручную. ## Выполнение Запустите следующую команду (замените "YOUR_MESSAGE" на отформатированную строку): diff --git a/.agent/skills/git-push/scripts/push_changes.sh b/.agent/skills/git-push/scripts/push_changes.sh index d8cdc9f..b7b598a 100755 --- a/.agent/skills/git-push/scripts/push_changes.sh +++ b/.agent/skills/git-push/scripts/push_changes.sh @@ -11,7 +11,13 @@ fi echo "🚀 Starting Local Push Sequence..." -# Выполняем цепочку команд локально +# Проверка на наличие изменений +if git diff-index --quiet HEAD --; then + echo "ℹ️ No changes to commit. Everything is up to date." + exit 0 +fi + +# Выполняем цепочку команд локально (SSH URL: ssh://git@gitea.zuev.company:2222/Zuev/magistr.git) git add . && \ git commit -m "$COMMIT_MSG" && \ git push origin main From 86a29f6419a4facbd4f744f7122084046c6a4a83 Mon Sep 17 00:00:00 2001 From: Zuev Date: Fri, 20 Feb 2026 00:48:03 +0300 Subject: [PATCH 05/14] feat(frontend): add dynamic animations to login and admin panel --- .agent/skills/git-commit-formatter/SKILL.md | 30 ----- .../skills/git-push/scripts/push_changes.sh | 2 +- frontend/admin/admin.css | 106 +++++++++++++++++- frontend/admin/admin.js | 24 ++++ frontend/index.html | 8 +- frontend/script.js | 26 ++++- frontend/student/index.html | 13 +++ frontend/style.css | 43 +++++++ frontend/teacher/index.html | 13 +++ 9 files changed, 228 insertions(+), 37 deletions(-) delete mode 100644 .agent/skills/git-commit-formatter/SKILL.md diff --git a/.agent/skills/git-commit-formatter/SKILL.md b/.agent/skills/git-commit-formatter/SKILL.md deleted file mode 100644 index d57aa1d..0000000 --- a/.agent/skills/git-commit-formatter/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: git-commit-formatter -description: Форматирует сообщения коммитов git в соответствии со спецификацией Conventional Commits. Используйте этот навык, когда пользователь просит закоммитить изменения или написать сообщение к коммиту. ---- - -# Навык форматирования коммитов Git - -При написании сообщения коммита вы ДОЛЖНЫ следовать спецификации Conventional Commits. - -## Формат -`[optional scope]: ` - -## Допустимые типы -- **feat**: Новая функциональность -- **fix**: Исправление ошибки -- **docs**: Изменения только в документации -- **style**: Изменения, не влияющие на смысл кода (пробелы, форматирование и т.д.) -- **refactor**: Изменение кода, которое не исправляет ошибку и не добавляет функциональность -- **perf**: Изменение кода, повышающее производительность -- **test**: Добавление недостающих тестов или исправление существующих -- **chore**: Изменения в процессе сборки или вспомогательных инструментах и библиотеках - -## Инструкции -1. Проанализируйте изменения, чтобы определить основной тип (`type`). -2. Определите область (`scope`), если это применимо (например, конкретный компонент или файл). -3. Напишите краткое описание (`description`) в повелительном наклонении (например, "add feature", а не "added feature"). -4. Если есть критические изменения, добавьте подвал, начинающийся с `BREAKING CHANGE:`. - -## Пример -`feat(auth): implement login with google` \ No newline at end of file diff --git a/.agent/skills/git-push/scripts/push_changes.sh b/.agent/skills/git-push/scripts/push_changes.sh index b7b598a..f5f4b68 100755 --- a/.agent/skills/git-push/scripts/push_changes.sh +++ b/.agent/skills/git-push/scripts/push_changes.sh @@ -17,7 +17,7 @@ if git diff-index --quiet HEAD --; then exit 0 fi -# Выполняем цепочку команд локально (SSH URL: ssh://git@gitea.zuev.company:2222/Zuev/magistr.git) +# Выполняем цепочку команд локально (SSH URL: ssh://git@192.168.1.87:2222/Zuev/magistr.git) git add . && \ git commit -m "$COMMIT_MSG" && \ git push origin main diff --git a/frontend/admin/admin.css b/frontend/admin/admin.css index 76d3749..9a7d458 100644 --- a/frontend/admin/admin.css +++ b/frontend/admin/admin.css @@ -134,6 +134,15 @@ body { .nav-item:hover { background: var(--bg-hover); color: var(--text-primary); + transform: translateX(4px); +} + +.nav-item svg { + transition: transform var(--transition); +} + +.nav-item:hover svg { + transform: scale(1.1); } .nav-item.active { @@ -199,12 +208,38 @@ body { } /* ===== Cards ===== */ +@keyframes slideUpCard { + from { + opacity: 0; + transform: translateY(15px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + .card { background: var(--bg-card); border: 1px solid var(--bg-card-border); border-radius: var(--radius-md); padding: 1.5rem; transition: background 0.4s ease, border-color 0.4s ease; + animation: slideUpCard 0.4s ease-out both; +} + +/* Staggered cards */ +.card:nth-child(1) { + animation-delay: 0.1s; +} + +.card:nth-child(2) { + animation-delay: 0.2s; +} + +.card:nth-child(3) { + animation-delay: 0.3s; } .card h2 { @@ -298,6 +333,18 @@ body { box-shadow: 0 4px 16px var(--accent-glow); } +@keyframes slideDownAlert { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + .form-alert { display: none; padding: 0.6rem 1rem; @@ -311,6 +358,7 @@ body { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.2); color: var(--error); + animation: slideDownAlert 0.3s ease-out both; } .form-alert.success { @@ -318,6 +366,7 @@ body { background: rgba(52, 211, 153, 0.1); border: 1px solid rgba(52, 211, 153, 0.2); color: var(--success); + animation: slideDownAlert 0.3s ease-out both; } /* ===== Table ===== */ @@ -347,8 +396,45 @@ tbody td { border-bottom: 1px solid rgba(255, 255, 255, 0.03); } +@keyframes slideInRow { + from { + opacity: 0; + transform: translateX(-10px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + tbody tr { transition: background var(--transition); + animation: slideInRow 0.3s ease-out both; +} + +tbody tr:nth-child(1) { + animation-delay: 0.05s; +} + +tbody tr:nth-child(2) { + animation-delay: 0.1s; +} + +tbody tr:nth-child(3) { + animation-delay: 0.15s; +} + +tbody tr:nth-child(4) { + animation-delay: 0.2s; +} + +tbody tr:nth-child(5) { + animation-delay: 0.25s; +} + +tbody tr:nth-child(n+6) { + animation-delay: 0.3s; } tbody tr:hover { @@ -477,11 +563,29 @@ tbody tr:hover { font-family: inherit; font-size: 0.8rem; cursor: pointer; - transition: background var(--transition); + transition: background var(--transition), transform var(--transition); } .btn-delete:hover { background: rgba(248, 113, 113, 0.2); + transform: scale(1.05); +} + +/* ===== Ripple Effect ===== */ +.ripple { + position: absolute; + border-radius: 50%; + transform: scale(0); + animation: admin-ripple 0.6s linear; + background-color: rgba(255, 255, 255, 0.3); + pointer-events: none; +} + +@keyframes admin-ripple { + to { + transform: scale(4); + opacity: 0; + } } /* ===== Mobile Menu Toggle ===== */ diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js index a373aa1..ba51454 100644 --- a/frontend/admin/admin.js +++ b/frontend/admin/admin.js @@ -16,6 +16,30 @@ const sidebar = document.querySelector('.sidebar'); const sidebarOverlay = document.getElementById('sidebar-overlay'); + // Global Ripple Effect + document.addEventListener('click', function (e) { + const btn = e.target.closest('.btn-create, .btn-delete, .btn-logout'); + if (!btn) return; + + const rect = btn.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const ripple = document.createElement('span'); + ripple.classList.add('ripple'); + ripple.style.left = `${x}px`; + ripple.style.top = `${y}px`; + + if (getComputedStyle(btn).position === 'static') { + btn.style.position = 'relative'; + } + btn.style.overflow = 'hidden'; + + btn.appendChild(ripple); + + setTimeout(() => ripple.remove(), 600); + }); + // Users const usersTbody = document.getElementById('users-tbody'); const createForm = document.getElementById('create-form'); diff --git a/frontend/index.html b/frontend/index.html index 65a4acf..68e1607 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -37,7 +37,7 @@
-
+
@@ -49,7 +49,7 @@
-
+
@@ -67,9 +67,9 @@
- + - + `).join(''); + } + + function renderEquipmentCheckboxes(equipments) { + if (!equipments.length) { + equipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; + return; + } + equipmentCheckboxes.innerHTML = equipments.map(eq => ` + + `).join(''); + } + + createEquipmentForm.addEventListener('submit', async (e) => { + e.preventDefault(); + hideAlert(createEquipmentAlert); + const name = document.getElementById('new-equipment-name').value.trim(); + if (!name) { showAlert(createEquipmentAlert, 'Введите название', 'error'); return; } + + try { + const res = await fetch('/api/equipments', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, + body: JSON.stringify({ name }), + }); + const data = await res.json(); + if (res.ok) { + showAlert(createEquipmentAlert, `Оборудование "${data.name}" добавлено`, 'success'); + createEquipmentForm.reset(); + loadEquipments(); + } else { + showAlert(createEquipmentAlert, data.message || 'Ошибка создания', 'error'); + } + } catch (e) { showAlert(createEquipmentAlert, 'Ошибка соединения', 'error'); } + }); + + equipmentsTbody.addEventListener('click', async (e) => { + const btn = e.target.closest('.btn-delete'); + if (!btn) return; + if (!confirm('Удалить оборудование?')) return; + try { + const res = await fetch('/api/equipments/' + btn.dataset.id, { + method: 'DELETE', + headers: { 'Authorization': 'Bearer ' + token }, + }); + if (res.ok) { + loadEquipments(); + } else { + const data = await res.json(); + alert(data.message || 'Ошибка удаления'); + } + } catch (e) { alert('Ошибка соединения'); } + }); + + // ============================================================ + // CLASSROOMS + // ============================================================ + + async function loadClassrooms() { + try { + const res = await fetch('/api/classrooms', { + headers: { 'Authorization': 'Bearer ' + token }, + }); + const classrooms = await res.json(); + renderClassrooms(classrooms); + } catch (e) { + classroomsTbody.innerHTML = 'Ошибка загрузки'; + } + } + + function renderClassrooms(classrooms) { + if (!classrooms.length) { + classroomsTbody.innerHTML = 'Нет аудиторий'; + return; + } + classroomsTbody.innerHTML = classrooms.map(c => { + const equipHtml = c.equipments && c.equipments.length + ? c.equipments.map(eq => escapeHtml(eq.name)).join(', ') + : '—'; + + return ` + + ${c.id} + ${escapeHtml(c.name)} + ${c.capacity} чел. + ${equipHtml} + +
+ + ${c.isAvailable ? 'Доступна' : 'Не доступна'} + + +
+ + + + + + `; + }).join(''); + } + + createClassroomForm.addEventListener('submit', async (e) => { + e.preventDefault(); + hideAlert(createClassroomAlert); + const name = document.getElementById('new-classroom-name').value.trim(); + const capacity = parseInt(document.getElementById('new-classroom-capacity').value, 10); + + const checkedBoxes = Array.from(equipmentCheckboxes.querySelectorAll('input:checked')); + const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10)); + + if (!name || isNaN(capacity)) { showAlert(createClassroomAlert, 'Заполните обязательные поля', 'error'); return; } + + try { + const res = await fetch('/api/classrooms', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, + body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: true }), + }); + const data = await res.json(); + if (res.ok) { + showAlert(createClassroomAlert, `Аудитория "${data.name}" добавлена`, 'success'); + createClassroomForm.reset(); + loadClassrooms(); + } else { + showAlert(createClassroomAlert, data.message || 'Ошибка создания', 'error'); + } + } catch (e) { showAlert(createClassroomAlert, 'Ошибка соединения', 'error'); } + }); + + classroomsTbody.addEventListener('click', async (e) => { + const btnDelete = e.target.closest('.btn-delete'); + const btnToggleStatus = e.target.closest('.btn-icon-toggle'); + const btnEdit = e.target.closest('.btn-edit-classroom'); + + if (btnDelete) { + if (!confirm('Удалить аудиторию?')) return; + try { + const res = await fetch('/api/classrooms/' + btnDelete.dataset.id, { + method: 'DELETE', + headers: { 'Authorization': 'Bearer ' + token }, + }); + if (res.ok) loadClassrooms(); + else alert('Ошибка удаления'); + } catch (err) { alert('Ошибка соединения'); } + } + + if (btnToggleStatus) { + const id = btnToggleStatus.dataset.id; + const currentStatus = btnToggleStatus.dataset.currentStatus === 'true'; + try { + const res = await fetch('/api/classrooms/' + id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, + body: JSON.stringify({ isAvailable: !currentStatus }), + }); + if (res.ok) loadClassrooms(); + else alert('Ошибка изменения статуса'); + } catch (err) { alert('Ошибка соединения'); } + } + + if (btnEdit) { + const id = btnEdit.dataset.id; + openEditClassroomModal(id); + } + }); + + let editingClassroomData = null; + + async function openEditClassroomModal(id) { + try { + const res = await fetch('/api/classrooms', { headers: { 'Authorization': 'Bearer ' + token } }); + const classrooms = await res.json(); + editingClassroomData = classrooms.find(c => c.id == id); + + if (!editingClassroomData) return; + + document.getElementById('edit-classroom-id').value = editingClassroomData.id; + document.getElementById('edit-classroom-name').value = editingClassroomData.name; + document.getElementById('edit-classroom-capacity').value = editingClassroomData.capacity; + + if (allEquipments.length) { + editEquipmentCheckboxes.innerHTML = allEquipments.map(eq => { + const isChecked = editingClassroomData.equipments.some(e => e.id === eq.id) ? 'checked' : ''; + return ` + + `; + }).join(''); + } else { + editEquipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; + } + + hideAlert(editClassroomAlert); + modalEditClassroom.classList.add('open'); + } catch (e) { + alert('Ошибка загрузки данных аудитории'); + } + } + + modalEditClassroomClose.addEventListener('click', () => { + modalEditClassroom.classList.remove('open'); + }); + + modalEditClassroom.addEventListener('click', (e) => { + if (e.target === modalEditClassroom) { + modalEditClassroom.classList.remove('open'); + } + }); + + editClassroomForm.addEventListener('submit', async (e) => { + e.preventDefault(); + hideAlert(editClassroomAlert); + const id = document.getElementById('edit-classroom-id').value; + const name = document.getElementById('edit-classroom-name').value.trim(); + const capacity = parseInt(document.getElementById('edit-classroom-capacity').value, 10); + + const checkedBoxes = Array.from(editEquipmentCheckboxes.querySelectorAll('input:checked')); + const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10)); + + if (!name || isNaN(capacity)) { showAlert(editClassroomAlert, 'Заполните обязательные поля', 'error'); return; } + + try { + const res = await fetch('/api/classrooms/' + id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, + body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: editingClassroomData.isAvailable }), + }); + const data = await res.json(); + if (res.ok) { + modalEditClassroom.classList.remove('open'); + showAlert(createClassroomAlert, `Аудитория "${data.name}" обновлена`, 'success'); + loadClassrooms(); + } else { + showAlert(editClassroomAlert, data.message || 'Ошибка обновления', 'error'); + } + } catch (e) { showAlert(editClassroomAlert, 'Ошибка соединения', 'error'); } + }); + // ============================================================ // LOGOUT & INIT // ============================================================ diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 1fa9ac6..fffe4ee 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -59,6 +59,21 @@ Формы обучения + + + + + + Оборудование + + + + + + Аудитории +
+ + + + + + + + + From be46fa2be274c5417ba4dc6ce3986b06edf0e261 Mon Sep 17 00:00:00 2001 From: Zuev Date: Fri, 20 Feb 2026 02:52:43 +0300 Subject: [PATCH 08/14] fix(deploy): update workflow with escaped bash string --- .agent/workflows/deploy-server.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.agent/workflows/deploy-server.md b/.agent/workflows/deploy-server.md index 9e535f1..47d2d84 100644 --- a/.agent/workflows/deploy-server.md +++ b/.agent/workflows/deploy-server.md @@ -9,14 +9,5 @@ description: Деплой на удаленный сервер 192.168.1.87 (Git // turbo-all 1. Синхронизировать код и, если изменился init.sql, пересобрать базу данных: ```bash -ssh root@192.168.1.87 "cd /root/magistr/program/ && \ - git fetch origin main && \ - CHANGED=\$(git diff --name-only HEAD origin/main | grep db/init/init.sql || true) && \ - git reset --hard origin/main && \ - if [ ! -z \"\$CHANGED\" ]; then \ - echo 'Обнаружены изменения в init.sql, удаляем базу данных...'; \ - docker compose down -v; \ - sudo rm -rf db/data; \ - fi && \ - docker compose up -d --build" +bash -c "ssh root@192.168.1.87 'cd /root/magistr/program/ && git fetch origin main && CHANGED=\$(git diff --name-only HEAD origin/main | grep db/init/init.sql || true) && git reset --hard origin/main && if [ ! -z \"\$CHANGED\" ]; then echo \"Обнаружены изменения в init.sql, удаляем базу данных...\"; docker compose down -v; sudo rm -rf db/data; fi && docker compose up -d --build'" ``` From c552d14909a2e507e28a33117c7ec2ba2567dd5f Mon Sep 17 00:00:00 2001 From: Zuev Date: Fri, 20 Feb 2026 02:53:47 +0300 Subject: [PATCH 09/14] feat(backend): implement equipments entities and modify db --- .agent/rules/main.md | 5 +- .../app/controller/ClassroomController.java | 109 ++++++++++++++++++ .../app/controller/EquipmentController.java | 51 ++++++++ .../com/magistr/app/dto/ClassroomRequest.java | 42 +++++++ .../magistr/app/dto/ClassroomResponse.java | 63 ++++++++++ .../java/com/magistr/app/model/Classroom.java | 70 +++++++++++ .../java/com/magistr/app/model/Equipment.java | 39 +++++++ .../app/repository/ClassroomRepository.java | 10 ++ .../app/repository/EquipmentRepository.java | 10 ++ db/init/init.sql | 40 ++++++- 10 files changed, 431 insertions(+), 8 deletions(-) create mode 100644 backend/src/main/java/com/magistr/app/controller/ClassroomController.java create mode 100644 backend/src/main/java/com/magistr/app/controller/EquipmentController.java create mode 100644 backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java create mode 100644 backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java create mode 100644 backend/src/main/java/com/magistr/app/model/Classroom.java create mode 100644 backend/src/main/java/com/magistr/app/model/Equipment.java create mode 100644 backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java create mode 100644 backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java diff --git a/.agent/rules/main.md b/.agent/rules/main.md index f354340..b5d778e 100644 --- a/.agent/rules/main.md +++ b/.agent/rules/main.md @@ -55,7 +55,7 @@ trigger: always_on ### 2. Управление ресурсами и топология - **Управление аудиториями**: - Указание вместимости. - - Указание тэгов оборудования (Проектор, ПК, Лаборатория). + - Привязка доступного оборудования (через сущность Equipments: Проектор, ПК, Лаборатория). - Установка статуса "Не доступно" (блокирует назначение пар в этот период). - **Управление группами**: - Управление списком студентов (и возможность деления на подгруппы). @@ -97,7 +97,8 @@ trigger: always_on ## Основные сущности базы данных (Data Entities) - **Users**: Хранение пользователей и их ролей (Администратор, Преподаватель, Студент) для управления доступом. - **Groups**: Группы студентов, их привязка к формам обучения. (Могут делиться на **подгруппы** для лабораторных и практик). -- **Classrooms**: Аудиторный фонд (название, вместимость, статус доступности, тэги оборудования). +- **Equipments**: Справочник оборудования. +- **Classrooms**: Аудиторный фонд (название, вместимость, статус доступности, привязанный список оборудования Equipments). - **Subjects**: Предметы/Дисциплины (Высшая математика, Физика, Базы данных и т.д.). - **Teacher_Subjects**: Связующая таблица (Many-to-Many), определяющая, какие дисциплины ведет конкретный преподаватель. - **Lesson_Types**: Типы занятий для валидации (Лекция, Практика, Лабораторная работа). diff --git a/backend/src/main/java/com/magistr/app/controller/ClassroomController.java b/backend/src/main/java/com/magistr/app/controller/ClassroomController.java new file mode 100644 index 0000000..8084f41 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/ClassroomController.java @@ -0,0 +1,109 @@ +package com.magistr.app.controller; + +import com.magistr.app.dto.ClassroomRequest; +import com.magistr.app.dto.ClassroomResponse; +import com.magistr.app.model.Classroom; +import com.magistr.app.model.Equipment; +import com.magistr.app.repository.ClassroomRepository; +import com.magistr.app.repository.EquipmentRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/api/classrooms") +public class ClassroomController { + + private final ClassroomRepository classroomRepository; + private final EquipmentRepository equipmentRepository; + + public ClassroomController(ClassroomRepository classroomRepository, EquipmentRepository equipmentRepository) { + this.classroomRepository = classroomRepository; + this.equipmentRepository = equipmentRepository; + } + + @GetMapping + public List getAllClassrooms() { + return classroomRepository.findAll().stream() + .map(this::mapToResponse) + .toList(); + } + + @PostMapping + public ResponseEntity createClassroom(@RequestBody ClassroomRequest request) { + if (request.getName() == null || request.getName().isBlank()) { + return ResponseEntity.badRequest().body(Map.of("message", "Название аудитории обязательно")); + } + if (request.getCapacity() == null || request.getCapacity() <= 0) { + return ResponseEntity.badRequest().body(Map.of("message", "Вместимость должна быть больше нуля")); + } + if (classroomRepository.findByName(request.getName().trim()).isPresent()) { + return ResponseEntity.badRequest().body(Map.of("message", "Аудитория с таким названием уже существует")); + } + + Classroom classroom = new Classroom(); + classroom.setName(request.getName().trim()); + classroom.setCapacity(request.getCapacity()); + classroom.setIsAvailable(request.getIsAvailable() != null ? request.getIsAvailable() : true); + + if (request.getEquipmentIds() != null && !request.getEquipmentIds().isEmpty()) { + List equipments = equipmentRepository.findAllById(request.getEquipmentIds()); + classroom.setEquipments(new java.util.HashSet<>(equipments)); + } + + classroomRepository.save(classroom); + return ResponseEntity.ok(mapToResponse(classroom)); + } + + @PutMapping("/{id}") + public ResponseEntity updateClassroom(@PathVariable Long id, @RequestBody ClassroomRequest request) { + Optional opt = classroomRepository.findById(id); + if (opt.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + Classroom classroom = opt.get(); + + if (request.getName() != null && !request.getName().isBlank() + && !classroom.getName().equals(request.getName().trim())) { + if (classroomRepository.findByName(request.getName().trim()).isPresent()) { + return ResponseEntity.badRequest() + .body(Map.of("message", "Аудитория с таким названием уже существует")); + } + classroom.setName(request.getName().trim()); + } + + if (request.getCapacity() != null && request.getCapacity() > 0) { + classroom.setCapacity(request.getCapacity()); + } + + if (request.getIsAvailable() != null) { + classroom.setIsAvailable(request.getIsAvailable()); + } + + if (request.getEquipmentIds() != null) { + List equipments = equipmentRepository.findAllById(request.getEquipmentIds()); + classroom.setEquipments(new java.util.HashSet<>(equipments)); + } + + classroomRepository.save(classroom); + return ResponseEntity.ok(mapToResponse(classroom)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteClassroom(@PathVariable Long id) { + if (!classroomRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + classroomRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Аудитория удалена")); + } + + private ClassroomResponse mapToResponse(Classroom c) { + return new ClassroomResponse(c.getId(), c.getName(), c.getCapacity(), c.getIsAvailable(), + new java.util.ArrayList<>(c.getEquipments())); + } +} diff --git a/backend/src/main/java/com/magistr/app/controller/EquipmentController.java b/backend/src/main/java/com/magistr/app/controller/EquipmentController.java new file mode 100644 index 0000000..f305efb --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/EquipmentController.java @@ -0,0 +1,51 @@ +package com.magistr.app.controller; + +import com.magistr.app.model.Equipment; +import com.magistr.app.repository.EquipmentRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/equipments") +public class EquipmentController { + + private final EquipmentRepository equipmentRepository; + + public EquipmentController(EquipmentRepository equipmentRepository) { + this.equipmentRepository = equipmentRepository; + } + + @GetMapping + public List getAllEquipments() { + return equipmentRepository.findAll(); + } + + @PostMapping + public ResponseEntity createEquipment(@RequestBody Map request) { + String name = request.get("name"); + if (name == null || name.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("message", "Название обязательно")); + } + if (equipmentRepository.findByName(name.trim()).isPresent()) { + return ResponseEntity.badRequest().body(Map.of("message", "Оборудование с таким названием уже существует")); + } + + Equipment equipment = new Equipment(); + equipment.setName(name.trim()); + equipmentRepository.save(equipment); + + return ResponseEntity.ok(equipment); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteEquipment(@PathVariable Long id) { + if (!equipmentRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + equipmentRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Оборудование удалено")); + } +} diff --git a/backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java b/backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java new file mode 100644 index 0000000..72927e1 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java @@ -0,0 +1,42 @@ +package com.magistr.app.dto; + +import java.util.List; + +public class ClassroomRequest { + private String name; + private Integer capacity; + private Boolean isAvailable; + private List equipmentIds; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getCapacity() { + return capacity; + } + + public void setCapacity(Integer capacity) { + this.capacity = capacity; + } + + public Boolean getIsAvailable() { + return isAvailable; + } + + public void setIsAvailable(Boolean isAvailable) { + this.isAvailable = isAvailable; + } + + public List getEquipmentIds() { + return equipmentIds; + } + + public void setEquipmentIds(List equipmentIds) { + this.equipmentIds = equipmentIds; + } +} diff --git a/backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java b/backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java new file mode 100644 index 0000000..35b46d5 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java @@ -0,0 +1,63 @@ +package com.magistr.app.dto; + +import com.magistr.app.model.Equipment; +import java.util.List; + +public class ClassroomResponse { + private Long id; + private String name; + private Integer capacity; + private Boolean isAvailable; + private List equipments; + + public ClassroomResponse() { + } + + public ClassroomResponse(Long id, String name, Integer capacity, Boolean isAvailable, List equipments) { + this.id = id; + this.name = name; + this.capacity = capacity; + this.isAvailable = isAvailable; + this.equipments = equipments; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getCapacity() { + return capacity; + } + + public void setCapacity(Integer capacity) { + this.capacity = capacity; + } + + public Boolean getIsAvailable() { + return isAvailable; + } + + public void setIsAvailable(Boolean isAvailable) { + this.isAvailable = isAvailable; + } + + public List getEquipments() { + return equipments; + } + + public void setEquipments(List equipments) { + this.equipments = equipments; + } +} diff --git a/backend/src/main/java/com/magistr/app/model/Classroom.java b/backend/src/main/java/com/magistr/app/model/Classroom.java new file mode 100644 index 0000000..67b2840 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/model/Classroom.java @@ -0,0 +1,70 @@ +package com.magistr.app.model; + +import jakarta.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "classrooms") +public class Classroom { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 50) + private String name; + + @Column(nullable = false) + private Integer capacity; + + @Column(name = "is_available", nullable = false) + private Boolean isAvailable = true; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "classroom_equipments", joinColumns = @JoinColumn(name = "classroom_id"), inverseJoinColumns = @JoinColumn(name = "equipment_id")) + private Set equipments = new HashSet<>(); + + public Classroom() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getCapacity() { + return capacity; + } + + public void setCapacity(Integer capacity) { + this.capacity = capacity; + } + + public Boolean getIsAvailable() { + return isAvailable; + } + + public void setIsAvailable(Boolean isAvailable) { + this.isAvailable = isAvailable; + } + + public Set getEquipments() { + return equipments; + } + + public void setEquipments(Set equipments) { + this.equipments = equipments; + } +} diff --git a/backend/src/main/java/com/magistr/app/model/Equipment.java b/backend/src/main/java/com/magistr/app/model/Equipment.java new file mode 100644 index 0000000..a550410 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/model/Equipment.java @@ -0,0 +1,39 @@ +package com.magistr.app.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "equipments") +public class Equipment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 50) + private String name; + + public Equipment() { + } + + public Equipment(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java b/backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java new file mode 100644 index 0000000..aee60f2 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java @@ -0,0 +1,10 @@ +package com.magistr.app.repository; + +import com.magistr.app.model.Classroom; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ClassroomRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java b/backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java new file mode 100644 index 0000000..8d5a445 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java @@ -0,0 +1,10 @@ +package com.magistr.app.repository; + +import com.magistr.app.model.Equipment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface EquipmentRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/db/init/init.sql b/db/init/init.sql index 4855da3..a2401e2 100644 --- a/db/init/init.sql +++ b/db/init/init.sql @@ -55,21 +55,49 @@ INSERT INTO lesson_types (name) VALUES ('Лабораторная работа') ON CONFLICT (name) DO NOTHING; +-- Оборудование +CREATE TABLE IF NOT EXISTS equipments ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL +); + +INSERT INTO equipments (name) VALUES +('Проектор'), +('ПК'), +('Лаборатория'), +('Интерактивная доска') +ON CONFLICT (name) DO NOTHING; + -- Аудитории CREATE TABLE IF NOT EXISTS classrooms ( id BIGSERIAL PRIMARY KEY, name VARCHAR(50) UNIQUE NOT NULL, capacity INT NOT NULL, - is_available BOOLEAN DEFAULT TRUE, - hardware_tags VARCHAR(255) -- например: "Проектор, ПК, Лаборатория" + is_available BOOLEAN DEFAULT TRUE ); -INSERT INTO classrooms (name, capacity, hardware_tags) VALUES -('101 Ленинская', 120, 'Проектор, Доска'), -('202 IT Lab', 20, 'ПК, Проектор, Лаборатория'), -('303 Обычная', 30, 'Доска') +INSERT INTO classrooms (name, capacity) VALUES +('101 Ленинская', 120), +('202 IT Lab', 20), +('303 Обычная', 30) ON CONFLICT (name) DO NOTHING; +-- Привязка оборудования к аудиториям (Many-to-Many) +CREATE TABLE IF NOT EXISTS classroom_equipments ( + classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE, + equipment_id BIGINT NOT NULL REFERENCES equipments(id) ON DELETE CASCADE, + PRIMARY KEY (classroom_id, equipment_id) +); + +-- Заполнение привязок оборудования (на основе ID базовых данных) +-- '101 Ленинская' -> Проектор (1), Интерактивная доска (4) +INSERT INTO classroom_equipments (classroom_id, equipment_id) VALUES +(1, 1), (1, 4), +-- '202 IT Lab' -> ПК (2), Проектор (1), Лаборатория (3) +(2, 2), (2, 1), (2, 3), +-- '303 Обычная' -> ничего +ON CONFLICT DO NOTHING; + -- ========================================== -- Связи для преподавателей и студентов -- ========================================== From eea444409e4a8e5648157b2249caa47e1ca6a10b Mon Sep 17 00:00:00 2001 From: Zuev Date: Fri, 20 Feb 2026 02:59:10 +0300 Subject: [PATCH 10/14] docs(rules): update database rules to match init.sql --- .agent/rules/main.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.agent/rules/main.md b/.agent/rules/main.md index b5d778e..e2209bc 100644 --- a/.agent/rules/main.md +++ b/.agent/rules/main.md @@ -96,10 +96,13 @@ trigger: always_on ## Основные сущности базы данных (Data Entities) - **Users**: Хранение пользователей и их ролей (Администратор, Преподаватель, Студент) для управления доступом. -- **Groups**: Группы студентов, их привязка к формам обучения. (Могут делиться на **подгруппы** для лабораторных и практик). +- **Education_Forms**: Формы обучения (Бакалавриат, Магистратура и т.д.). +- **Student_Groups**: Группы студентов, их привязка к формам обучения `education_forms(id)`. +- **Subgroups**: Подгруппы студентов конкретной группы `group_id` для лабораторных и практик. - **Equipments**: Справочник оборудования. -- **Classrooms**: Аудиторный фонд (название, вместимость, статус доступности, привязанный список оборудования Equipments). +- **Classrooms**: Аудиторный фонд (название, вместимость, статус доступности). +- **Classroom_Equipments**: Связующая таблица (Many-to-Many), определяющая какое оборудование находится в аудитории. - **Subjects**: Предметы/Дисциплины (Высшая математика, Физика, Базы данных и т.д.). -- **Teacher_Subjects**: Связующая таблица (Many-to-Many), определяющая, какие дисциплины ведет конкретный преподаватель. +- **Teacher_Subjects**: Связующая таблица (Many-to-Many), определяющая, какие дисциплины ведет конкретный преподаватель `users(id)`. - **Lesson_Types**: Типы занятий для валидации (Лекция, Практика, Лабораторная работа). -- **Lessons / Schedules**: Сами занятия (пары). Каждая запись связывает преподавателя, аудиторию, группу (или подгруппу), предмет (`subject_id`), тип занятия (`lesson_type`) и конкретное время. \ No newline at end of file +- **Lessons / Schedules**: Сами занятия (пары). Каждая запись связывает преподавателя, аудиторию, группу (и/или подгруппу), предмет (`subject_id`), тип занятия (`lesson_type_id`), день недели, четность недели и конкретное время (start_time, end_time). \ No newline at end of file From 74d937f6dc5ee2da75598ca03196e8e2989e2b12 Mon Sep 17 00:00:00 2001 From: Zuev Date: Sat, 21 Feb 2026 01:35:56 +0300 Subject: [PATCH 11/14] feat: complete UI redesign with glassmorphism and custom multi-select equipment dropdown --- .agent/rules/main.md | 3 +- frontend/admin/admin.css | 366 ++++++++++++++++++++++++++++++------ frontend/admin/admin.js | 53 ++++++ frontend/admin/index.html | 37 +++- frontend/student/index.html | 190 ++++++++++++++----- frontend/style.css | 187 +++++++++++------- frontend/teacher/index.html | 191 +++++++++++++++---- 7 files changed, 820 insertions(+), 207 deletions(-) diff --git a/.agent/rules/main.md b/.agent/rules/main.md index e2209bc..e4dc0de 100644 --- a/.agent/rules/main.md +++ b/.agent/rules/main.md @@ -8,7 +8,8 @@ trigger: always_on Этот проект представляет собой веб-сайт системы управления университетским расписанием. - **Роль**: Образовательная платформа для управления расписанием. - **Язык**: Смешанный (Java Backend + Web Frontend). -- **Публичный URL**: https://magistr.zuev.company +- **Публичный URL прода**: https://magistr.zuev.company +- **Локальный URL проекта**: localhost:80 ## Структура директорий и обязанности Проект следует определенной структуре папок. Вы должны придерживаться этих путей: diff --git a/frontend/admin/admin.css b/frontend/admin/admin.css index 0e522bd..f178ab8 100644 --- a/frontend/admin/admin.css +++ b/frontend/admin/admin.css @@ -8,45 +8,56 @@ } :root { - --bg-primary: #0f0f1a; - --bg-sidebar: rgba(255, 255, 255, 0.03); - --bg-card: rgba(255, 255, 255, 0.05); - --bg-card-border: rgba(255, 255, 255, 0.08); - --bg-input: rgba(255, 255, 255, 0.06); - --bg-input-focus: rgba(255, 255, 255, 0.1); + /* Deep dark premium background */ + --bg-primary: #0a0a0f; + --bg-sidebar: rgba(255, 255, 255, 0.02); + --bg-card: rgba(255, 255, 255, 0.03); + --bg-card-border: rgba(255, 255, 255, 0.05); + --bg-input: rgba(255, 255, 255, 0.04); + --bg-input-focus: rgba(255, 255, 255, 0.08); --bg-hover: rgba(255, 255, 255, 0.06); - --text-primary: #f0f0f5; - --text-secondary: #9ca3af; - --text-placeholder: #6b7280; - --accent: #6366f1; - --accent-hover: #818cf8; - --accent-glow: rgba(99, 102, 241, 0.35); - --error: #f87171; - --success: #34d399; - --warning: #fbbf24; - --radius-sm: 8px; - --radius-md: 12px; - --transition: 0.2s ease; + + /* Typography */ + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --text-placeholder: #475569; + + /* Vibrant Accents */ + --accent: #8b5cf6; + --accent-hover: #a78bfa; + --accent-glow: rgba(139, 92, 246, 0.4); + --accent-secondary: #ec4899; + + /* Status Colors */ + --error: #ef4444; + --success: #10b981; + --warning: #f59e0b; + + /* Spatial */ + --radius-sm: 10px; + --radius-md: 16px; + --transition: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } /* ===== Light Theme ===== */ [data-theme="light"] { - --bg-primary: #e8eaef; - --bg-sidebar: rgba(255, 255, 255, 0.88); - --bg-card: rgba(255, 255, 255, 0.95); - --bg-card-border: rgba(0, 0, 0, 0.22); - --bg-input: rgba(0, 0, 0, 0.08); - --bg-input-focus: rgba(0, 0, 0, 0.12); - --bg-hover: rgba(0, 0, 0, 0.08); + --bg-primary: #f8fafc; + --bg-sidebar: rgba(255, 255, 255, 0.7); + --bg-card: rgba(255, 255, 255, 0.7); + --bg-card-border: rgba(0, 0, 0, 0.08); + --bg-input: rgba(0, 0, 0, 0.03); + --bg-input-focus: rgba(0, 0, 0, 0.06); + --bg-hover: rgba(0, 0, 0, 0.05); --text-primary: #0f172a; - --text-secondary: #374151; - --text-placeholder: #6b7280; + --text-secondary: #475569; + --text-placeholder: #94a3b8; --accent: #6366f1; --accent-hover: #4f46e5; - --accent-glow: rgba(99, 102, 241, 0.25); - --error: #dc2626; - --success: #16a34a; - --warning: #d97706; + --accent-glow: rgba(99, 102, 241, 0.3); + --accent-secondary: #d946ef; + --error: #ef4444; + --success: #10b981; + --warning: #f59e0b; } [data-theme="light"] .form-group select option, @@ -59,6 +70,10 @@ background: rgba(99, 102, 241, 0.18); } +[data-theme="light"] .custom-multi-select .dropdown-menu { + background: rgba(255, 255, 255, 0.98); +} + [data-theme="light"] .form-group input, [data-theme="light"] .form-group select, [data-theme="light"] .filter-row select { @@ -85,9 +100,11 @@ body { /* ===== Sidebar ===== */ .sidebar { - width: 240px; + width: 260px; min-height: 100vh; background: var(--bg-sidebar); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); border-right: 1px solid var(--bg-card-border); display: flex; flex-direction: column; @@ -96,7 +113,7 @@ body { top: 0; bottom: 0; z-index: 10; - transition: background 0.4s ease, border-color 0.4s ease; + transition: background 0.4s ease, border-color 0.4s ease, transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } .sidebar-header { @@ -121,14 +138,31 @@ body { .nav-item { display: flex; align-items: center; - gap: 0.6rem; - padding: 0.65rem 0.8rem; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin-bottom: 0.25rem; border-radius: var(--radius-sm); color: var(--text-secondary); text-decoration: none; - font-size: 0.9rem; + font-size: 0.95rem; font-weight: 500; - transition: background var(--transition), color var(--transition); + transition: all var(--transition); + position: relative; + overflow: hidden; +} + +.nav-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--accent); + border-radius: 0 4px 4px 0; + transform: scaleY(0); + transition: transform var(--transition); + opacity: 0; } .nav-item:hover { @@ -137,12 +171,23 @@ body { transform: translateX(4px); } +.nav-item.active { + background: rgba(139, 92, 246, 0.12); + color: var(--accent-hover); +} + +.nav-item.active::before { + transform: scaleY(1); + opacity: 1; +} + .nav-item svg { transition: transform var(--transition); } -.nav-item:hover svg { - transform: scale(1.1); +.nav-item:hover svg, +.nav-item.active svg { + transform: scale(1.15) rotate(-5deg); } /* Checkbox list styling */ @@ -247,7 +292,7 @@ body { /* ===== Main ===== */ .main { flex: 1; - margin-left: 240px; + margin-left: 260px; min-height: 100vh; } @@ -290,11 +335,38 @@ body { .card { background: var(--bg-card); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); border: 1px solid var(--bg-card-border); border-radius: var(--radius-md); - padding: 1.5rem; - transition: background 0.4s ease, border-color 0.4s ease; - animation: slideUpCard 0.4s ease-out both; + padding: 1.75rem; + position: relative; + overflow: visible; + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + animation: slideUpCard 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) both; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + opacity: 0; + transition: opacity var(--transition); +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1); + border-color: rgba(255, 255, 255, 0.12); +} + +.card:hover::before { + opacity: 1; } /* Staggered cards */ @@ -310,6 +382,11 @@ body { animation-delay: 0.3s; } +/* Specific Cards */ +.create-card { + z-index: 10; +} + .card h2 { font-size: 0.8rem; font-weight: 600; @@ -345,26 +422,43 @@ body { .form-group input, .form-group select { width: 100%; - padding: 0.65rem 0.8rem; + padding: 0.75rem 1rem; background: var(--bg-input); - border: 1px solid transparent; + border: 1px solid var(--bg-card-border); border-radius: var(--radius-sm); color: var(--text-primary); font-family: inherit; - font-size: 0.9rem; + font-size: 0.95rem; outline: none; - transition: background var(--transition), border-color var(--transition), box-shadow var(--transition); + transition: all var(--transition); } .form-group input::placeholder { color: var(--text-placeholder); + transition: opacity var(--transition); } .form-group input:focus, .form-group select:focus { background: var(--bg-input-focus); border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-glow); + box-shadow: 0 0 0 4px var(--accent-glow); + transform: translateY(-1px); +} + +.form-group input:focus::placeholder { + opacity: 0.5; +} + +/* Hide Number Arrows */ +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; } .form-group select { @@ -382,23 +476,48 @@ body { } .btn-create { - padding: 0.65rem 1.5rem; - background: linear-gradient(135deg, var(--accent), #8b5cf6); + position: relative; + overflow: hidden; + padding: 0.75rem 1.75rem; + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); border: none; border-radius: var(--radius-sm); color: #fff; font-family: inherit; - font-size: 0.9rem; + font-size: 0.95rem; font-weight: 600; + letter-spacing: 0.02em; cursor: pointer; white-space: nowrap; - transition: transform var(--transition), box-shadow var(--transition); - box-shadow: 0 2px 10px var(--accent-glow); + transition: all var(--transition); + box-shadow: 0 4px 15px var(--accent-glow); +} + +.btn-create::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(rgba(255, 255, 255, 0.2), transparent); + border-radius: inherit; + opacity: 0; + transition: opacity var(--transition); } .btn-create:hover { - transform: translateY(-1px); - box-shadow: 0 4px 16px var(--accent-glow); + transform: translateY(-2px); + box-shadow: 0 8px 25px var(--accent-glow); +} + +.btn-create:hover::before { + opacity: 1; +} + +.btn-create:active { + transform: translateY(1px); + box-shadow: 0 2px 10px var(--accent-glow); } @keyframes slideDownAlert { @@ -439,7 +558,7 @@ body { /* ===== Table ===== */ .table-wrap { - overflow-x: auto; + overflow-x: visible; } table { @@ -459,9 +578,22 @@ thead th { } tbody td { - padding: 0.7rem 0.8rem; - font-size: 0.9rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.03); + padding: 0.85rem 1rem; + font-size: 0.95rem; + border-bottom: 1px solid var(--bg-card-border); + transition: background var(--transition); +} + +@keyframes slideInRow { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } } @keyframes slideInRow { @@ -683,6 +815,122 @@ tbody tr:hover { } /* ===== Theme Toggle Button ===== */ +.theme-toggle { + width: 40px; + height: 40px; + border: none; + border-radius: 50%; + background: var(--bg-card); + border: 1px solid var(--bg-card-border); + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + transition: all var(--transition); + z-index: 100; + flex-shrink: 0; +} + +.theme-toggle svg { + width: 20px; + height: 20px; + transition: transform 0.4s ease; +} + +.theme-toggle:hover { + transform: scale(1.1); + box-shadow: 0 4px 16px var(--accent-glow); +} + +.theme-toggle:active { + transform: scale(0.95); +} + +/* ===== Custom Multi Select ===== */ +.custom-multi-select { + position: relative; + width: 100%; +} + +.custom-multi-select .select-box { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: inherit; + font-size: 0.95rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + transition: all var(--transition); +} + +.custom-multi-select .select-box.active { + background: var(--bg-input-focus); + border-color: var(--accent); + box-shadow: 0 0 0 4px var(--accent-glow); +} + +.custom-multi-select .dropdown-icon { + transition: transform var(--transition); +} + +.custom-multi-select .select-box.active .dropdown-icon { + transform: rotate(180deg); +} + +.custom-multi-select .dropdown-menu { + position: absolute; + top: calc(100% + 5px); + left: 0; + width: 100%; + background: rgba(15, 15, 26, 0.98); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-md); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + padding: 0.75rem; + z-index: 9999; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all var(--transition); + max-height: 250px; + overflow-y: auto; +} + +.custom-multi-select .dropdown-menu.open { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.checkbox-group-vertical { + display: flex; + flex-direction: column; + gap: 8px; +} + +.checkbox-group-vertical .checkbox-item { + padding: 6px 8px; + border-radius: 6px; + transition: background var(--transition); + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.checkbox-group-vertical .checkbox-item:hover { + background: var(--bg-hover); +} /* ===== Modals ===== */ .modal-overlay { diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js index 599935c..af4a7f6 100644 --- a/frontend/admin/admin.js +++ b/frontend/admin/admin.js @@ -73,6 +73,56 @@ const createEquipmentAlert = document.getElementById('create-equipment-alert'); const equipmentCheckboxes = document.getElementById('equipment-checkboxes'); + // --- Multi-select logic --- + function updateSelectText(containerId, textId) { + const container = document.getElementById(containerId); + const textEl = document.getElementById(textId); + if (!container || !textEl) return; + const checked = Array.from(container.querySelectorAll('input:checked')); + if (checked.length === 0) { + textEl.textContent = 'Выберите оборудование...'; + } else if (checked.length === 1) { + textEl.textContent = checked[0].parentElement.textContent.trim(); + } else { + textEl.textContent = `Выбрано: ${checked.length}`; + } + } + + function initMultiSelect(boxId, menuId, textId, checkboxContainerId) { + const box = document.getElementById(boxId); + const menu = document.getElementById(menuId); + const container = document.getElementById(checkboxContainerId); + if (!box || !menu || !container) return; + + box.addEventListener('click', (e) => { + e.stopPropagation(); + const isOpen = menu.classList.contains('open'); + document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open')); + document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active')); + if (!isOpen) { + menu.classList.add('open'); + box.classList.add('active'); + } + }); + + menu.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + container.addEventListener('change', () => { + updateSelectText(checkboxContainerId, textId); + }); + } + + initMultiSelect('equipment-select-box', 'equipment-dropdown-menu', 'equipment-select-text', 'equipment-checkboxes'); + initMultiSelect('edit-equipment-select-box', 'edit-equipment-dropdown-menu', 'edit-equipment-select-text', 'edit-equipment-checkboxes'); + + document.addEventListener('click', () => { + document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open')); + document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active')); + }); + // -------------------------- + const navItems = document.querySelectorAll('.nav-item[data-tab]'); const tabContents = document.querySelectorAll('.tab-content'); @@ -427,6 +477,7 @@ ${escapeHtml(eq.name)} `).join(''); + updateSelectText('equipment-checkboxes', 'equipment-select-text'); } createEquipmentForm.addEventListener('submit', async (e) => { @@ -541,6 +592,7 @@ if (res.ok) { showAlert(createClassroomAlert, `Аудитория "${data.name}" добавлена`, 'success'); createClassroomForm.reset(); + updateSelectText('equipment-checkboxes', 'equipment-select-text'); loadClassrooms(); } else { showAlert(createClassroomAlert, data.message || 'Ошибка создания', 'error'); @@ -611,6 +663,7 @@ } else { editEquipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; } + updateSelectText('edit-equipment-checkboxes', 'edit-equipment-select-text'); hideAlert(editClassroomAlert); modalEditClassroom.classList.add('open'); diff --git a/frontend/admin/index.html b/frontend/admin/index.html index fffe4ee..41b3e09 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -292,12 +292,24 @@
-
+
-
- -

Загрузка...

+
+
+ Выберите оборудование... + + + +
+
@@ -351,8 +363,21 @@
-
- +
+
+ Выберите + оборудование... + + + +
+
diff --git a/frontend/student/index.html b/frontend/student/index.html index 2b35dd5..131afe2 100644 --- a/frontend/student/index.html +++ b/frontend/student/index.html @@ -1,11 +1,10 @@ - Панель студента - +