Добавление режима для учителя
This commit is contained in:
parent
05c04c49ed
commit
23ea2ed09f
|
|
@ -0,0 +1,287 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Скрипт для создания группы, репозитория и копирования кода в GitLab
|
||||
# с назначением прав пользователю из Keycloak
|
||||
|
||||
set -e # Прерывать выполнение при ошибках
|
||||
|
||||
# === КОНФИГУРАЦИЯ ===
|
||||
|
||||
# GitLab Private Token с правами администратора
|
||||
PRIVATE_TOKEN=$1
|
||||
# Пользователь из Keycloak (должен существовать в GitLab)
|
||||
KEYCLOAK_USERNAME=$2
|
||||
USER_ID=${KEYCLOAK_USERNAME}
|
||||
|
||||
GITLAB_URL="https://gitlab.oa2.ru" # URL вашего GitLab
|
||||
|
||||
# Данные для новой группы и репозитория
|
||||
GROUP_NAME="group-${USER_ID}"
|
||||
GROUP_PATH="group-${USER_ID}" # Может отличаться от имени
|
||||
GROUP_DESCRIPTION="Группа для пользователя ${USER_ID}"
|
||||
|
||||
# Данные для нового репозитория
|
||||
PROJECT_NAME="task1"
|
||||
PROJECT_DESCRIPTION="Репозиторий с кодом task1"
|
||||
|
||||
# Внешний репозиторий для копирования
|
||||
SOURCE_REPO_URL="https://git.oa2.ru/dzyk/task1" # Замените на реальный URL
|
||||
|
||||
# Уровень доступа для пользователя (owner = 50)
|
||||
ACCESS_LEVEL=50
|
||||
|
||||
# Временная директория для клонирования
|
||||
TEMP_DIR="/tmp/gitlab-migration-$(date +%s)"
|
||||
|
||||
# === ФУНКЦИИ ===
|
||||
|
||||
# Функция для логирования
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Функция для выполнения HTTP запросов к GitLab API
|
||||
gitlab_api() {
|
||||
local method=$1
|
||||
local endpoint=$2
|
||||
local data=$3
|
||||
|
||||
local curl_cmd="curl -s -X $method \
|
||||
'$GITLAB_URL/api/v4$endpoint' \
|
||||
-H 'Private-Token: $PRIVATE_TOKEN' \
|
||||
-H 'Content-Type: application/json'"
|
||||
|
||||
if [ ! -z "$data" ]; then
|
||||
curl_cmd="$curl_cmd -d '$data'"
|
||||
fi
|
||||
|
||||
eval $curl_cmd
|
||||
}
|
||||
|
||||
# Функция для проверки ошибок API
|
||||
check_api_error() {
|
||||
local response=$1
|
||||
local operation=$2
|
||||
|
||||
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
|
||||
error_msg=$(echo "$response" | jq -r '.error')
|
||||
log "Ошибка при $operation: $error_msg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | jq -e '.message' > /dev/null 2>&1; then
|
||||
error_msg=$(echo "$response" | jq -r '.message')
|
||||
if [ "$error_msg" != "null" ] && [ ! -z "$error_msg" ]; then
|
||||
log "Ошибка при $operation: $error_msg"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# === ОСНОВНОЙ СКРИПТ ===
|
||||
|
||||
log "Начало выполнения скрипта"
|
||||
|
||||
# 1. Проверка зависимостей
|
||||
log "Проверка зависимостей..."
|
||||
for cmd in curl jq git; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
log "Ошибка: $cmd не установлен"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
log "Все зависимости установлены"
|
||||
|
||||
# 2. Поиск пользователя из Keycloak
|
||||
log "Поиск пользователя '$KEYCLOAK_USERNAME' в GitLab..."
|
||||
user_search=$(gitlab_api "GET" "/users?username=$KEYCLOAK_USERNAME")
|
||||
if ! check_api_error "$user_search" "поиске пользователя"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
user_id=$(echo "$user_search" | jq -r '.[0].id')
|
||||
user_username=$(echo "$user_search" | jq -r '.[0].username')
|
||||
|
||||
if [ "$user_id" = "null" ] || [ -z "$user_id" ]; then
|
||||
log "Ошибка: Пользователь '$KEYCLOAK_USERNAME' не найден в GitLab"
|
||||
log "Убедитесь, что пользователь вошел через Keycloak хотя бы один раз"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Найден пользователь: $user_username (ID: $user_id)"
|
||||
|
||||
# 4. Создание группы
|
||||
log "Проверка существования группы '$GROUP_NAME'..."
|
||||
group_check=$(gitlab_api "GET" "/groups/$GROUP_PATH")
|
||||
if echo "$group_check" | jq -e '.id' > /dev/null 2>&1; then
|
||||
group_id=$(echo "$group_check" | jq -r '.id')
|
||||
log "Группа '$GROUP_NAME' уже существует (ID: $group_id)"
|
||||
else
|
||||
log "Создание группы '$GROUP_NAME'..."
|
||||
group_data=$(jq -n \
|
||||
--arg name "$GROUP_NAME" \
|
||||
--arg path "$GROUP_PATH" \
|
||||
--arg desc "$GROUP_DESCRIPTION" \
|
||||
--arg visibility "private" \
|
||||
'{
|
||||
name: $name,
|
||||
path: $path,
|
||||
description: $desc,
|
||||
visibility: $visibility,
|
||||
lfs_enabled: true,
|
||||
request_access_enabled: true
|
||||
}')
|
||||
|
||||
create_group=$(gitlab_api "POST" "/groups" "$group_data")
|
||||
if ! check_api_error "$create_group" "создании группы"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
group_id=$(echo "$create_group" | jq -r '.id')
|
||||
log "Группа создана успешно (ID: $group_id)"
|
||||
fi
|
||||
|
||||
# 5. Создание проекта в группе
|
||||
log "Проверка существования проекта '$PROJECT_NAME' в группе..."
|
||||
project_check=$(gitlab_api "GET" "/projects/$GROUP_PATH%2F$PROJECT_NAME")
|
||||
if echo "$project_check" | jq -e '.id' > /dev/null 2>&1; then
|
||||
project_id=$(echo "$project_check" | jq -r '.id')
|
||||
log "Проект '$PROJECT_NAME' уже существует (ID: $project_id)"
|
||||
else
|
||||
log "Создание проекта '$PROJECT_NAME' в группе..."
|
||||
project_data=$(jq -n \
|
||||
--arg name "$PROJECT_NAME" \
|
||||
--arg path "$PROJECT_NAME" \
|
||||
--arg desc "$PROJECT_DESCRIPTION" \
|
||||
--arg namespace_id "$group_id" \
|
||||
--arg visibility "private" \
|
||||
'{
|
||||
name: $name,
|
||||
path: $path,
|
||||
description: $desc,
|
||||
namespace_id: $namespace_id,
|
||||
visibility: $visibility,
|
||||
initialize_with_readme: false,
|
||||
lfs_enabled: true,
|
||||
container_registry_enabled: true
|
||||
}')
|
||||
|
||||
create_project=$(gitlab_api "POST" "/projects" "$project_data")
|
||||
if ! check_api_error "$create_project" "создании проекта"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
project_id=$(echo "$create_project" | jq -r '.id')
|
||||
project_ssh_url=$(echo "$create_project" | jq -r '.ssh_url_to_repo')
|
||||
project_http_url=$(echo "$create_project" | jq -r '.http_url_to_repo')
|
||||
log "Проект создан успешно (ID: $project_id)"
|
||||
log "URL проекта: $project_http_url"
|
||||
fi
|
||||
|
||||
# 6. Назначение прав пользователю как owner группы
|
||||
log "Проверка прав пользователя $user_username в группе..."
|
||||
group_members=$(gitlab_api "GET" "/groups/$group_id/members")
|
||||
user_in_group=$(echo "$group_members" | jq --arg user_id "$user_id" '.[] | select(.id == ($user_id|tonumber))')
|
||||
|
||||
if [ -z "$user_in_group" ]; then
|
||||
log "Назначение прав owner пользователю $user_username в группе..."
|
||||
member_data=$(jq -n \
|
||||
--arg user_id "$user_id" \
|
||||
--arg access_level "$ACCESS_LEVEL" \
|
||||
'{
|
||||
user_id: $user_id,
|
||||
access_level: ($access_level|tonumber)
|
||||
}')
|
||||
|
||||
add_member=$(gitlab_api "POST" "/groups/$group_id/members" "$member_data")
|
||||
if ! check_api_error "$add_member" "добавлении пользователя в группу"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Пользователю $user_username назначены права owner в группе"
|
||||
else
|
||||
current_access=$(echo "$user_in_group" | jq -r '.access_level')
|
||||
if [ "$current_access" != "$ACCESS_LEVEL" ]; then
|
||||
log "Обновление прав пользователя $user_username с уровня $current_access на $ACCESS_LEVEL..."
|
||||
member_data=$(jq -n \
|
||||
--arg access_level "$ACCESS_LEVEL" \
|
||||
'{
|
||||
access_level: ($access_level|tonumber)
|
||||
}')
|
||||
|
||||
update_member=$(gitlab_api "PUT" "/groups/$group_id/members/$user_id" "$member_data")
|
||||
if ! check_api_error "$update_member" "обновлении прав пользователя"; then
|
||||
exit 1
|
||||
fi
|
||||
log "Права пользователя обновлены"
|
||||
else
|
||||
log "Пользователь $user_username уже имеет права owner в группе"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 7. Клонирование и копирование кода
|
||||
log "Создание временной директории: $TEMP_DIR"
|
||||
mkdir -p "$TEMP_DIR"
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
log "Клонирование исходного репозитория: $SOURCE_REPO_URL"
|
||||
if ! git clone --bare "$SOURCE_REPO_URL" source-repo.git; then
|
||||
log "Ошибка при клонировании исходного репозитория"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Клонирование целевого репозитория GitLab"
|
||||
# Получаем URL репозитория с токеном для записи
|
||||
repo_url_with_token=$(echo "$project_http_url" | sed "s|https://|https://gitlab-ci-token:$PRIVATE_TOKEN@|")
|
||||
if ! git clone "$repo_url_with_token" target-repo; then
|
||||
log "Ошибка при клонировании целевого репозитория"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd target-repo
|
||||
|
||||
log "Добавление remote исходного репозитория"
|
||||
git remote add source ../source-repo.git
|
||||
|
||||
log "Получение данных из исходного репозитория"
|
||||
git fetch source
|
||||
|
||||
log "Объединение всех веток из исходного репозитория"
|
||||
for branch in $(git branch -r | grep 'source/' | sed 's|source/||'); do
|
||||
if [ "$branch" != "HEAD" ] && [ "$branch" != "->" ]; then
|
||||
log " Копирование ветки: $branch"
|
||||
git checkout -b "$branch" "source/$branch" 2>/dev/null || git branch "$branch" "source/$branch"
|
||||
fi
|
||||
done
|
||||
|
||||
# Возвращаемся в основную ветку (если есть)
|
||||
if git show-ref --verify --quiet refs/heads/main; then
|
||||
git checkout main
|
||||
elif git show-ref --verify --quiet refs/heads/master; then
|
||||
git checkout master
|
||||
fi
|
||||
|
||||
log "Отправка всех веток в GitLab"
|
||||
git push --all origin
|
||||
git push --tags origin
|
||||
|
||||
log "Очистка временных файлов"
|
||||
cd /
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# 8. Проверка результата
|
||||
log "Проверка созданной структуры..."
|
||||
final_check=$(gitlab_api "GET" "/projects/$project_id")
|
||||
project_name=$(echo "$final_check" | jq -r '.name_with_namespace')
|
||||
web_url=$(echo "$final_check" | jq -r '.web_url')
|
||||
|
||||
log "=== ОТЧЕТ ==="
|
||||
log "Группа: $GROUP_NAME (ID: $group_id)"
|
||||
log "Проект: $project_name (ID: $project_id)"
|
||||
log "URL проекта: $web_url"
|
||||
log "Пользователь с правами owner: $user_username"
|
||||
log "Код скопирован из: $SOURCE_REPO_URL"
|
||||
log ""
|
||||
log "Скрипт успешно выполнен!"
|
||||
|
|
@ -10,6 +10,7 @@ import ru.oa2.lti.repository.entities.LMSContent;
|
|||
import ru.oa2.lti.repository.entities.Task;
|
||||
import ru.oa2.lti.service.jwt.JwtService;
|
||||
import ru.oa2.lti.service.jwt.Payload;
|
||||
import ru.oa2.lti.service.results.dto.ResultResponse;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
|
@ -55,18 +56,13 @@ public class ApplicationService {
|
|||
}
|
||||
}
|
||||
|
||||
public String saveResult(String body) {
|
||||
public ResultResponse updateTask(String body) {
|
||||
return new ResultResponse("success");
|
||||
}
|
||||
|
||||
return "{\n" +
|
||||
" \"success\": true,\n" +
|
||||
" \"message\": \"Результат успешно получен и обработан\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"contextId\": \"ctx-12345\",\n" +
|
||||
" \"participantId\": \"usr-67890\",\n" +
|
||||
" \"submittedText\": \"console.log('Hello');\",\n" +
|
||||
" \"timestamp\": \"2025-12-14T17:55:30Z\",\n" +
|
||||
" \"status\": \"processed\"\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
public ResultResponse saveResult(String body) {
|
||||
|
||||
//TODO заполнять из результатов проверок check скрипта
|
||||
return new ResultResponse("success");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ru.oa2.lti.controller;
|
|||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
|
@ -11,6 +12,8 @@ import ru.oa2.lti.infrastructure.LMSService;
|
|||
import ru.oa2.lti.model.LtiLogin;
|
||||
import ru.oa2.lti.service.jwt.Payload;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/tool/lti")
|
||||
|
|
@ -63,6 +66,11 @@ public class LaunchController {
|
|||
return "redirect:/tool/lti/select";
|
||||
}
|
||||
|
||||
@PostMapping("/task")
|
||||
public ResponseEntity updateTask() {
|
||||
|
||||
return service.updateTask("TODO");
|
||||
}
|
||||
|
||||
@GetMapping("/task")
|
||||
public String showDockerTas(Model model,
|
||||
|
|
@ -81,6 +89,9 @@ public class LaunchController {
|
|||
model.addAttribute("description", data);
|
||||
}
|
||||
|
||||
if (Objects.requireNonNull(payload).getCoach()) {
|
||||
return "task-editor";
|
||||
}
|
||||
return "task";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ public class ResultController {
|
|||
jwtService.getTokenPayload(idToken)
|
||||
);
|
||||
|
||||
//TODO возвращать json
|
||||
return ResponseEntity
|
||||
.accepted()
|
||||
.body(service.saveResult(body));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Редактор задачи LTI</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
.container-fluid {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
.header-section {
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e4e8;
|
||||
}
|
||||
.content-section {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
background-color: #fdfefe;
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.footer {
|
||||
padding: 15px 20px;
|
||||
background-color: #f1f3f5;
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.status-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
.status-success {
|
||||
color: #28a745;
|
||||
}
|
||||
.status-failed {
|
||||
color: #dc3545;
|
||||
}
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.submit-btn {
|
||||
padding: 10px 24px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.code-area {
|
||||
font-family: 'Courier New', monospace;
|
||||
background-color: #f0f4f8;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Заголовок -->
|
||||
<div class="header-section">
|
||||
<h4>Редактор задачи</h4>
|
||||
</div>
|
||||
|
||||
<!-- Основное содержимое -->
|
||||
<div class="content-section">
|
||||
|
||||
<!-- Поле: Описание задачи (редактируемое) -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Описание задачи (HTML)</label>
|
||||
<textarea id="description" class="code-area" placeholder="Введите HTML-описание задачи..."><th:block th:utext="${description ?: ''}"/></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Поле: initScript -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">initScript (JavaScript)</label>
|
||||
<textarea id="initScript" class="code-area" placeholder="JavaScript-код для инициализации среды..."><th:block th:text="${initScript ?: ''}"/></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Поле: checkScript -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">checkScript (JavaScript)</label>
|
||||
<textarea id="checkScript" class="code-area" placeholder="JavaScript-код для проверки результата..."><th:block th:text="${checkScript ?: ''}"/></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Футер: статус + кнопки -->
|
||||
<div class="footer">
|
||||
<!-- Статус -->
|
||||
<div id="statusContainer" class="status-container" style="display: none;">
|
||||
<div id="statusIcon" class="status-icon">
|
||||
<i id="icon" class="fa-solid"></i>
|
||||
</div>
|
||||
<span id="statusLabel" class="status-label"></span>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div>
|
||||
<button id="saveBtn" class="btn btn-success submit-btn me-2">
|
||||
<i class="fas fa-save"></i> Сохранить задачу
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Скрытые поля -->
|
||||
<input type="hidden" id="contextId" th:value="${contextId}" />
|
||||
<input type="hidden" id="participantId" th:value="${participantId}" />
|
||||
|
||||
<!-- Скрипт отправки -->
|
||||
<script th:src="@{/tool/lti/js/main.js}"></script>
|
||||
<script>
|
||||
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||
const contextId = document.getElementById('contextId').value;
|
||||
const description = document.getElementById('description').value;
|
||||
const initScript = document.getElementById('initScript').value;
|
||||
const checkScript = document.getElementById('checkScript').value;
|
||||
|
||||
const payload = {
|
||||
contextId,
|
||||
description,
|
||||
initScript,
|
||||
checkScript
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/tool/lti/task', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
const statusContainer = document.getElementById('statusContainer');
|
||||
const statusIcon = document.getElementById('icon');
|
||||
const statusLabel = document.getElementById('statusLabel');
|
||||
|
||||
if (response.ok) {
|
||||
statusIcon.className = 'fa-solid fa-check-circle';
|
||||
statusIcon.classList.add('status-success');
|
||||
statusLabel.textContent = 'Задача успешно сохранена';
|
||||
statusContainer.style.display = 'flex';
|
||||
statusContainer.classList.remove('status-failed');
|
||||
statusContainer.classList.add('status-success');
|
||||
} else {
|
||||
throw new Error(result.message || 'Ошибка сохранения');
|
||||
}
|
||||
} catch (error) {
|
||||
const statusContainer = document.getElementById('statusContainer');
|
||||
const statusIcon = document.getElementById('icon');
|
||||
const statusLabel = document.getElementById('statusLabel');
|
||||
|
||||
statusIcon.className = 'fa-solid fa-times-circle';
|
||||
statusIcon.classList.add('status-failed');
|
||||
statusLabel.textContent = 'Ошибка: ' + error.message;
|
||||
statusContainer.style.display = 'flex';
|
||||
statusContainer.classList.remove('status-success');
|
||||
statusContainer.classList.add('status-failed');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue