diff --git a/backend/Dockerfile b/backend/Dockerfile
index e4b3343..23e1969 100755
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -4,6 +4,7 @@ COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
+RUN curl -L -o opentelemetry-javaagent.jar https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
FROM eclipse-temurin:17-jre-alpine
@@ -13,5 +14,6 @@ USER spring:spring
WORKDIR /app
COPY --from=build /app/target/app.jar app.jar
+COPY --from=build /app/opentelemetry-javaagent.jar opentelemetry-javaagent.jar
EXPOSE 8080
-ENTRYPOINT ["java", "-jar", "app.jar"]
+ENTRYPOINT ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "app.jar"]
diff --git a/backend/pom.xml b/backend/pom.xml
index 2a160d5..54b385f 100755
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -56,6 +56,13 @@
h2
runtime
+
+
+
+ io.opentelemetry
+ opentelemetry-api
+ 1.49.0
+
diff --git a/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java b/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java
index 924222b..e85c961 100755
--- a/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java
+++ b/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java
@@ -9,6 +9,8 @@ import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException;
import java.util.Map;
+import org.slf4j.MDC;
+import io.opentelemetry.api.trace.Span;
/**
* Interceptor: извлекает поддомен из Host header и кладёт в TenantContext.
@@ -48,6 +50,8 @@ public class TenantInterceptor implements HandlerInterceptor {
// (нужно чтобы админ мог добавить тенант даже если его домен не настроен)
if (path.startsWith("/api/database")) {
TenantContext.setCurrentTenant(tenant);
+ MDC.put("tenant.id", tenant);
+ Span.current().setAttribute("tenant.id", tenant);
log.debug("Database API request, tenant '{}' (no strict check)", tenant);
return true;
}
@@ -66,6 +70,8 @@ public class TenantInterceptor implements HandlerInterceptor {
}
TenantContext.setCurrentTenant(tenant);
+ MDC.put("tenant.id", tenant);
+ Span.current().setAttribute("tenant.id", tenant);
log.debug("Resolved tenant '{}' from Host '{}'", tenant, host);
return true;
}
@@ -73,6 +79,7 @@ public class TenantInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
+ MDC.remove("tenant.id");
}
private String resolveTenant(String host) {
diff --git a/backend/src/main/resources/db/migration/V2__departmentCreate.sql b/backend/src/main/resources/db/migration/V2__departmentCreate.sql
new file mode 100644
index 0000000..51a41fb
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V2__departmentCreate.sql
@@ -0,0 +1,223 @@
+-- ===============================
+-- Создание таблицы кафедр
+-- ===============================
+
+CREATE TABLE IF NOT EXISTS departments (
+ id BIGSERIAL UNIQUE PRIMARY KEY NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ code BIGINT UNIQUE NOT NULL
+);
+
+INSERT INTO departments (name, code) VALUES
+ ('Кафедра ИБ', 1),
+ ('Кафедра ВТ', 2),
+ ('Кафедра КТ', 3);
+
+COMMENT ON TABLE departments IS 'Кафедры';
+-- ===============================
+-- Создание таблицы специальностей
+-- ===============================
+
+CREATE TABLE IF NOT EXISTS specialties (
+ id BIGSERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ specialty_code VARCHAR(255) NOT NULL
+);
+
+INSERT INTO specialties (name, specialty_code) VALUES
+ ('Информационная безопасность', '10.03.01'),
+ ('Информатика и вычислительная техника', '09.03.01'),
+ ('Программная инженерия', '09.03.04');
+
+COMMENT ON TABLE specialties IS 'Специальности';
+-- ===============================
+-- Обновление таблицы дисциплин
+-- ===============================
+
+ALTER TABLE subjects
+ADD COLUMN IF NOT EXISTS department_id BIGINT REFERENCES departments(id);
+
+UPDATE subjects
+SET department_id = 1
+WHERE department_id IS NULL;
+
+ALTER TABLE subjects
+ALTER COLUMN department_id SET NOT NULL;
+
+COMMENT ON TABLE subjects IS 'Дисциплины';
+-- ===============================
+-- Обновление таблицы групп
+-- ===============================
+
+ALTER TABLE student_groups
+ADD COLUMN IF NOT EXISTS department_id BIGINT REFERENCES departments(id);
+
+UPDATE student_groups
+SET department_id = 1
+WHERE department_id IS NULL;
+
+ALTER TABLE student_groups
+ALTER COLUMN department_id SET NOT NULL;
+
+COMMENT ON TABLE student_groups IS 'Группы';
+-- ===============================
+-- Обновление таблицы пользователей
+-- ===============================
+
+ALTER TABLE users
+ADD COLUMN IF NOT EXISTS full_name VARCHAR(255),
+ADD COLUMN IF NOT EXISTS job_title VARCHAR(255),
+ADD COLUMN IF NOT EXISTS department_id BIGINT REFERENCES departments(id);
+
+UPDATE users
+SET (full_name, job_title, department_id) =
+ ('Иванов Админ Иванович', 'Доцент', 1)
+WHERE id = 1;
+
+UPDATE users
+SET (full_name, job_title, department_id) =
+ ('Петров Препод Петрович', 'Профессор', 2)
+WHERE id = 2;
+
+ALTER TABLE users
+ALTER COLUMN full_name SET NOT NULL,
+ALTER COLUMN job_title SET NOT NULL,
+ALTER COLUMN department_id SET NOT NULL;
+
+COMMENT ON TABLE users IS 'Пользователи';
+-- ===============================
+-- Создание таблицы данных расписания
+-- ===============================
+
+CREATE TABLE IF NOT EXISTS schedule_data (
+ id BIGSERIAL PRIMARY KEY,
+ department_id BIGINT NOT NULL REFERENCES departments(id),
+ semester INT NOT NULL,
+ group_id BIGINT NOT NULL REFERENCES student_groups(id),
+ subjects_id BIGINT NOT NULL REFERENCES subjects(id),
+ lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
+ number_of_hours INT NOT NULL,
+ is_division BOOLEAN NOT NULL DEFAULT FALSE,
+ teacher_id BIGINT NOT NULL REFERENCES users(id),
+ semester_type VARCHAR(255) NOT NULL,
+ period VARCHAR(255) NOT NULL
+);
+
+INSERT INTO schedule_data (department_id, semester, group_id, subjects_id, lesson_type_id, number_of_hours, is_division, teacher_id, semester_type, period)
+VALUES (1, 1, 1, 1, 3, 2, true, 1, 'Весенний', '2024/2025'),
+ (2, 4, 2, 3, 2, 1, false, 2, 'Осенний', '2025/2026'),
+ (3, 5, 1, 2, 1, 3, true, 1, 'Весенний', '2023/2024');
+
+COMMENT ON TABLE schedule_data IS 'Данные к составлению расписания';
+COMMENT ON COLUMN schedule_data.department_id IS 'Идентификатор кафедры';
+COMMENT ON COLUMN schedule_data.semester IS 'Номер семестра';
+COMMENT ON COLUMN schedule_data.group_id IS 'Идентификатор группы';
+COMMENT ON COLUMN schedule_data.subjects_id IS 'Идентификатор предмета';
+COMMENT ON COLUMN schedule_data.lesson_type_id IS 'Идентификатор типа занятия';
+COMMENT ON COLUMN schedule_data.number_of_hours IS 'Количество часов';
+COMMENT ON COLUMN schedule_data.is_division IS 'Является ли занятие разделенным';
+COMMENT ON COLUMN schedule_data.teacher_id IS 'Идентификатор преподавателя';
+COMMENT ON COLUMN schedule_data.semester_type IS 'Тип семестра (Весенний, Осенний)';
+COMMENT ON COLUMN schedule_data.period IS 'Период занятий (год/год)';
+
+COMMENT ON TABLE education_forms IS 'Формы обучения';
+COMMENT ON TABLE subgroups IS 'Подгруппы';
+COMMENT ON TABLE lesson_types IS 'Типы занятий';
+COMMENT ON TABLE equipments IS 'Оборудование';
+COMMENT ON TABLE classrooms IS 'Аудитории';
+COMMENT ON TABLE classroom_equipments IS 'Привязка оборудования к аудиториям';
+COMMENT ON TABLE teacher_subjects IS 'Привязка преподавателей к дисциплинам';
+COMMENT ON TABLE teacher_lesson_types IS 'Типы занятий преподавателя';
+
+
+COMMENT ON COLUMN users.id IS 'ID пользователя';
+COMMENT ON COLUMN users.username IS 'Логин пользователя';
+COMMENT ON COLUMN users.password IS 'Хэш пароля пользователя';
+COMMENT ON COLUMN users.role IS 'Роль пользователя';
+COMMENT ON COLUMN users.created_at IS 'Дата и время создания';
+COMMENT ON COLUMN users.updated_at IS 'Дата и время последнего обновления';
+
+COMMENT ON COLUMN education_forms.id IS 'ID формы обучения';
+COMMENT ON COLUMN education_forms.name IS 'Название формы обучения';
+COMMENT ON COLUMN education_forms.description IS 'Описание';
+COMMENT ON COLUMN education_forms.created_at IS 'Дата и время создания';
+
+COMMENT ON COLUMN student_groups.id IS 'ID учебной группы';
+COMMENT ON COLUMN student_groups.name IS 'Название группы';
+COMMENT ON COLUMN student_groups.group_size IS 'Количество студентов';
+COMMENT ON COLUMN student_groups.education_form_id IS 'ID формы обучения, к которой относится группа';
+COMMENT ON COLUMN student_groups.course IS 'Курс';
+COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
+
+COMMENT ON COLUMN subgroups.id IS 'ID подгруппы';
+COMMENT ON COLUMN subgroups.group_id IS 'ID учебной группы, к которой относится подгруппа';
+COMMENT ON COLUMN subgroups.name IS 'Название подгруппы';
+COMMENT ON COLUMN subgroups.student_capacity IS 'Количество студентов в подгруппе';
+
+COMMENT ON COLUMN subjects.id IS 'ID предмета';
+COMMENT ON COLUMN subjects.name IS 'Название предмета';
+COMMENT ON COLUMN subjects.code IS 'Код предмета';
+COMMENT ON COLUMN subjects.description IS 'Описание предмета';
+COMMENT ON COLUMN subjects.created_at IS 'Дата и время создания';
+
+COMMENT ON COLUMN lesson_types.id IS 'ID урока';
+COMMENT ON COLUMN lesson_types.name IS 'Название типа урока';
+COMMENT ON COLUMN lesson_types.color_code IS 'Цветовой код для типа урока';
+COMMENT ON COLUMN lesson_types.duration_minutes IS 'Длительность урока в минутах';
+
+COMMENT ON COLUMN equipments.id IS 'ID оборудования';
+COMMENT ON COLUMN equipments.name IS 'Название оборудования';
+COMMENT ON COLUMN equipments.description IS 'Описание оборудования';
+COMMENT ON COLUMN equipments.inventory_number IS 'Инвентарный номер оборудования';
+
+COMMENT ON COLUMN classrooms.id IS 'ID аудитории';
+COMMENT ON COLUMN classrooms.name IS 'Название аудитории';
+COMMENT ON COLUMN classrooms.capacity IS 'Вместимость аудитории';
+COMMENT ON COLUMN classrooms.building IS 'Корпус';
+COMMENT ON COLUMN classrooms.floor IS 'Этаж';
+COMMENT ON COLUMN classrooms.is_available IS 'Доступность аудитории';
+COMMENT ON COLUMN classrooms.description IS 'Описание аудитории';
+COMMENT ON COLUMN classrooms.created_at IS 'Дата и время создания';
+
+COMMENT ON COLUMN classroom_equipments.classroom_id IS 'ID аудитории';
+COMMENT ON COLUMN classroom_equipments.equipment_id IS 'ID оборудования';
+COMMENT ON COLUMN classroom_equipments.quantity IS 'Дата и время создания';
+COMMENT ON COLUMN classroom_equipments.notes IS 'Примечания к записи';
+
+COMMENT ON COLUMN teacher_subjects.user_id IS 'ID преподавателя';
+COMMENT ON COLUMN teacher_subjects.subject_id IS 'ID предмета';
+COMMENT ON COLUMN teacher_subjects.qualification_level IS 'Уровень квалификации преподавателя';
+COMMENT ON COLUMN teacher_subjects.experience_years IS 'Опыт преподавания';
+
+COMMENT ON COLUMN lessons.id IS 'ID урока';
+COMMENT ON COLUMN lessons.teacher_id IS 'Идентификатор преподавателя, который проводит урок';
+COMMENT ON COLUMN lessons.group_id IS 'ID группы, в которой проходит урок';
+COMMENT ON COLUMN lessons.subject_id IS 'ID предмета, который преподается';
+COMMENT ON COLUMN lessons.lesson_format IS 'Формат урока';
+COMMENT ON COLUMN lessons.type_lesson IS 'Тип урока';
+COMMENT ON COLUMN lessons.classroom_id IS 'ID аудитории, в которой проходит урок';
+COMMENT ON COLUMN lessons.day IS 'День недели, в который проходит урок';
+COMMENT ON COLUMN lessons.week IS 'Номер недели, в которой проходит урок';
+COMMENT ON COLUMN lessons.time IS 'Время урока';
+
+COMMENT ON COLUMN departments.id IS 'ID кафедры';
+COMMENT ON COLUMN departments.name IS 'Название кафедры';
+COMMENT ON COLUMN departments.code IS 'Код кафедры';
+
+COMMENT ON COLUMN specialties.id IS 'ID специальности';
+COMMENT ON COLUMN specialties.name IS 'Название специальности';
+COMMENT ON COLUMN specialties.specialty_code IS 'Код специальности';
+
+COMMENT ON COLUMN teacher_lesson_types.user_id IS 'ID преподавателя';
+COMMENT ON COLUMN teacher_lesson_types.subject_id IS 'ID предмета';
+COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';
+
+COMMENT ON COLUMN schedule_data.id IS 'ID записи данных расписания';
+
+COMMENT ON COLUMN subjects.department_id IS 'ID кафедры';
+
+COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
+
+COMMENT ON COLUMN users.full_name IS 'ФИО пользователя';
+COMMENT ON COLUMN users.job_title IS 'Должность пользователя';
+COMMENT ON COLUMN users.department_id IS 'ID кафедры';
\ No newline at end of file
diff --git a/frontend/script.js b/frontend/script.js
index 6603b09..d427f1e 100755
--- a/frontend/script.js
+++ b/frontend/script.js
@@ -1,6 +1,32 @@
(() => {
'use strict';
+ // --- OpenTelemetry Frontend Instrumentation ---
+ // Загружаем OTel Web SDK динамически через esm.sh, чтобы не ломать старый Vanilla JS (без type="module")
+ import('https://esm.sh/@opentelemetry/sdk-trace-web').then(async ({ WebTracerProvider, BatchSpanProcessor }) => {
+ const { OTLPTraceExporter } = await import('https://esm.sh/@opentelemetry/exporter-trace-otlp-http');
+ const { getWebAutoInstrumentations } = await import('https://esm.sh/@opentelemetry/auto-instrumentations-web');
+ const { registerInstrumentations } = await import('https://esm.sh/@opentelemetry/instrumentation');
+ const { Resource } = await import('https://esm.sh/@opentelemetry/resources');
+
+ const exporter = new OTLPTraceExporter({
+ url: window.location.origin + '/otel/v1/traces' // Трафик пойдет через ваш Caddy Proxy
+ });
+
+ const provider = new WebTracerProvider({
+ resource: new Resource({ 'service.name': 'magistr-frontend' }),
+ });
+
+ provider.addSpanProcessor(new BatchSpanProcessor(exporter));
+ provider.register();
+
+ registerInstrumentations({
+ instrumentations: [getWebAutoInstrumentations()]
+ });
+ console.log("SigNoz (OpenTelemetry) инициализирован во фронтенде.");
+ }).catch(e => console.error("Ошибка загрузки OTel:", e));
+ // ----------------------------------------------
+
const form = document.getElementById('login-form');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');