From ca9993f459acd88db88a38da7d024524fa8023d1 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 21:14:06 +0300 Subject: [PATCH 01/17] rm unused workflow --- .github/workflows/collect_commits.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/collect_commits.yml diff --git a/.github/workflows/collect_commits.yml b/.github/workflows/collect_commits.yml deleted file mode 100644 index 4082c7b..0000000 --- a/.github/workflows/collect_commits.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Collect Commits -on: - push: - -jobs: - collect-commits: - runs-on: [self-hosted, mse] - steps: - - name: Collect info - run: | - cd /data/ - mkdir -p '${{github.repository}}' - - echo '${{ toJson(github) }}' > "./run.json" - - python3 get_info.py From 1ee3cca107b484b8d9bbe200b93b83009b8ef619 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 21:15:11 +0300 Subject: [PATCH 02/17] rename "link" form input to "work_title" --- client/pages/index.html | 4 ++-- client/scripts/index.js | 28 ++++++++++++++-------------- client/scripts/media.js | 2 +- server/app.py | 10 +++++----- server/static/filter.js | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/client/pages/index.html b/client/pages/index.html index 7bf9d87..45abc2d 100644 --- a/client/pages/index.html +++ b/client/pages/index.html @@ -24,8 +24,8 @@

Локальный прокторинг

Нет отчества - - + +
diff --git a/client/scripts/index.js b/client/scripts/index.js index 157a939..da9dab2 100644 --- a/client/scripts/index.js +++ b/client/scripts/index.js @@ -24,7 +24,7 @@ const inputElements = { name: document.querySelector('#name_input'), surname: document.querySelector('#surname_input'), patronymic: document.querySelector('#patronymic_input'), - link: document.querySelector('#link_input') + work_title: document.querySelector('#link_input') }; const buttonElements = { @@ -80,20 +80,20 @@ const validationRules = { message: "Группа должна содержать ровно 4 цифры. Пример: '1234'" }, name: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ-]*$/, - message: "Имя должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире. Пример: 'Иван'" + regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ- ]*$/, + message: "Имя должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире/пробел. Пример: 'Иван'" }, surname: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ-]*$/, - message: "Фамилия должна начинаться с заглавной буквы и содержать только русские/латинские буквы и тире. Пример: 'Иванов'" + regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ- ]*$/, + message: "Фамилия должна начинаться с заглавной буквы и содержать только русские/латинские буквы и тире/пробел. Пример: 'Иванов'" }, patronymic: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ-]*$/, - message: "Отчество должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире. Пример: 'Иванович'" + regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ- ]*$/, + message: "Отчество должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире/пробел. Пример: 'Иванович'" }, - link: { + work_title: { regex: /.+/, - message: "Ссылка на комнату не должна быть пустой." + message: "Название работы не должно быть пустым." } }; @@ -165,7 +165,7 @@ async function saveInputValues() { surname: inputElements.surname.value, patronymic: inputElements.patronymic.value, noPatronymicChecked: noPatronymicCheckbox.checked, - link: inputElements.link.value + work_title: inputElements.work_title.value } }); logClientAction({ action: "Save input values" }); @@ -474,7 +474,7 @@ async function startRecCallback() { name: inputElements.name.value, surname: inputElements.surname.value, patronymic: noPatronymicCheckbox.checked ? "Без_отчества" : inputElements.patronymic.value.trim(), - link: inputElements.link.value + work_title: inputElements.work_title.value }; chrome.runtime.sendMessage({ @@ -699,10 +699,10 @@ async function uploadVideo(files) { await chrome.storage.local.remove("metadata"); await chrome.storage.local.set({"session_status" : "need_init"}); - inputElements.link.value = ""; - inputElements.link.classList.remove('input-valid', 'input-invalid'); + inputElements.work_title.value = ""; + inputElements.work_title.classList.remove('input-valid', 'input-invalid'); await saveInputValues(); - logClientAction("Clear link field"); + logClientAction("Clear work title field"); } }); } \ No newline at end of file diff --git a/client/scripts/media.js b/client/scripts/media.js index 8a1152f..edb3639 100644 --- a/client/scripts/media.js +++ b/client/scripts/media.js @@ -696,7 +696,7 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { formData.append('name', message.formData.name || ''); formData.append('surname', message.formData.surname || ''); formData.append('patronymic', message.formData.patronymic || ''); - formData.append('link', message.formData.link || ''); + formData.append('work_title', message.formData.work_title || ''); function formDataToObject(formData) { const obj = {}; diff --git a/server/app.py b/server/app.py index 8589dcd..50c9a48 100644 --- a/server/app.py +++ b/server/app.py @@ -32,10 +32,10 @@ def start_session(): surname = data.get("surname") name = data.get("name") patronymic = data.get("patronymic") - link = data.get("link") + work_title = data.get("work_title") - if not (group and surname and name and patronymic and link): - return jsonify({"error": "Поля 'group', 'surname', 'name', 'patronymic', 'link' обязательны для заполнения"}), 400 + if not (group and surname and name and patronymic and work_title): + return jsonify({"error": "Поля 'group', 'surname', 'name', 'patronymic', 'work_title' обязательны для заполнения"}), 400 session_start = datetime.now(timezone.utc) # Форматирование даты @@ -48,7 +48,7 @@ def start_session(): "surname": surname, "name": name, "patronymic": patronymic, - "link": link, + "work_title": work_title, "session_date_start": session_date_start, "session_time_start": session_time_start, "session_date_end": None, @@ -60,7 +60,7 @@ def start_session(): "logs_path": None } - sessions_collection.insert_one(session_data) + sessions_collection.insert_one(session_data) # TODO: add MongoModel return jsonify({"id": str(id)}), 201 except Exception as e: diff --git a/server/static/filter.js b/server/static/filter.js index ffe9909..2a35b04 100644 --- a/server/static/filter.js +++ b/server/static/filter.js @@ -44,7 +44,7 @@ document.addEventListener("DOMContentLoaded", function () { `${fields2column}`, "status", "metadata", - "link" + "work_title" ]; // Создаем заголовок таблицы From 0bcf24b4356520ed2ab2eb4f76333db53e0bc5d4 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 21:15:47 +0300 Subject: [PATCH 03/17] rm server files attaching to release --- .github/workflows/release_attacher.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/release_attacher.yml b/.github/workflows/release_attacher.yml index 973242a..3b8d224 100644 --- a/.github/workflows/release_attacher.yml +++ b/.github/workflows/release_attacher.yml @@ -16,15 +16,8 @@ jobs: - name: Create archive client run: zip -r client_${{ github.event.release.tag_name }}.zip client/* - - name: Create archive server - run: zip -r server_${{ github.event.release.tag_name }}.zip server/* - - name: Attach archives to GitHub Release uses: softprops/action-gh-release@v2 - # Для softprops/action-gh-release@v2 токен по умолчанию не нужен - # env: - # GITHUB_TOKEN: ${{ github.token }} with: files: | client_${{ github.event.release.tag_name }}.zip - server_${{ github.event.release.tag_name }}.zip From cde0ec99df42abc74f4d66356baa4e8b6a650cb1 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 21:38:50 +0300 Subject: [PATCH 04/17] minify text on success getPermissionsMedia --- client/scripts/media.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/scripts/media.js b/client/scripts/media.js index edb3639..da107eb 100644 --- a/client/scripts/media.js +++ b/client/scripts/media.js @@ -673,9 +673,6 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { "Для удобства уведомление о доступе к вашему экрану можно скрыть или передвинуть. НЕЛЬЗЯ НАЖИМАТЬ НА «Закрыть доступ».", "НЕЛЬЗЯ ОБНОВЛЯТЬ, ЗАКРЫВАТЬ СЛУЖЕБНУЮ ВКЛАДКУ во время записи! НЕЛЬЗЯ ЗАКРЫВАТЬ БРАУЗЕР во время записи!", "Предпросмотр будет отключен. Его можно включить по кнопке на служебной вкладке расширения. По умолчанию звук выключен и включается в плеере.", - "Во всплывающем окне расширения прокторинга можно найти статистику начала, продолжительности записи, а также разрешений.", - "В случае потери разрешений запись будет прервана и ее необходимо будет начать заново.", - "Расширение сообщит Вам обо всем необходимом. Удачной работы!", "", "Нажмите на кнопку «Начать запись» во всплывающем окне " + "расширения прокторинга, когда будете готовы.",], From 243f6288a2ab620db49789bd08d18489bcb80c9c Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 21:39:01 +0300 Subject: [PATCH 05/17] some page text fixes --- client/pages/index.html | 2 +- client/pages/media.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/index.html b/client/pages/index.html index 45abc2d..8be2642 100644 --- a/client/pages/index.html +++ b/client/pages/index.html @@ -24,7 +24,7 @@

Локальный прокторинг

Нет отчества
- + diff --git a/client/pages/media.html b/client/pages/media.html index ea6b06e..98590cd 100644 --- a/client/pages/media.html +++ b/client/pages/media.html @@ -8,7 +8,7 @@
-

Не закрывайте, не обновляйте эту вкладку! Служебная вкладка!

+

Не закрывайте, не обновляйте эту вкладку! Служебная вкладка для работы локального прокторинга!

From 9da3c85d40eaf5469e0db08ec030016fed8ef2dc Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 21:41:17 +0300 Subject: [PATCH 06/17] add step to minify code in release attacher workflow --- .github/workflows/release_attacher.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release_attacher.yml b/.github/workflows/release_attacher.yml index 3b8d224..951804a 100644 --- a/.github/workflows/release_attacher.yml +++ b/.github/workflows/release_attacher.yml @@ -13,6 +13,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Minimize client code + run: | + apt install -y nodejs npm jq + npm install -g terser + cd client + for i in */*.js; do terser $i --compress -o $i; done + jq -c < settings.json > tmp.json + rm settings.json && mv tmp.json settings.json + cd - + - name: Create archive client run: zip -r client_${{ github.event.release.tag_name }}.zip client/* From 3db59a842c1ea85dc00a06a0e0ff931f7f82b9b2 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 21:53:08 +0300 Subject: [PATCH 07/17] add validationStringRegExp (only cirillic) --- client/scripts/index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/client/scripts/index.js b/client/scripts/index.js index da9dab2..16f12ad 100644 --- a/client/scripts/index.js +++ b/client/scripts/index.js @@ -74,22 +74,24 @@ const bStates = { } } +const validationStringRegExp = /^[А-ЯЁ][а-яёА-ЯЁ -]*$/; + const validationRules = { group: { regex: /^\d{4}$/, message: "Группа должна содержать ровно 4 цифры. Пример: '1234'" }, name: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ- ]*$/, - message: "Имя должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире/пробел. Пример: 'Иван'" + regex: validationStringRegExp, + message: "Имя должно начинаться с заглавной буквы и содержать только кириллицу и тире/пробел. Пример: 'Иван'" }, surname: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ- ]*$/, - message: "Фамилия должна начинаться с заглавной буквы и содержать только русские/латинские буквы и тире/пробел. Пример: 'Иванов'" + regex: validationStringRegExp, + message: "Фамилия должна начинаться с заглавной буквы и содержать только кириллицу и тире/пробел. Пример: 'Иванов'" }, patronymic: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ- ]*$/, - message: "Отчество должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире/пробел. Пример: 'Иванович'" + regex: validationStringRegExp, + message: "Отчество должно начинаться с заглавной буквы и содержать только кириллицу и тире/пробел. Пример: 'Иванович'" }, work_title: { regex: /.+/, From 5d45217cfd2c937fae62c5168e790f543f0ed786 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 22:05:35 +0300 Subject: [PATCH 08/17] update work_title elements --- client/pages/index.html | 2 +- client/scripts/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/index.html b/client/pages/index.html index 8be2642..ba6b04d 100644 --- a/client/pages/index.html +++ b/client/pages/index.html @@ -25,7 +25,7 @@

Локальный прокторинг

Нет отчества
- +
diff --git a/client/scripts/index.js b/client/scripts/index.js index 16f12ad..e65d599 100644 --- a/client/scripts/index.js +++ b/client/scripts/index.js @@ -24,7 +24,7 @@ const inputElements = { name: document.querySelector('#name_input'), surname: document.querySelector('#surname_input'), patronymic: document.querySelector('#patronymic_input'), - work_title: document.querySelector('#link_input') + work_title: document.querySelector('#work_title_input') }; const buttonElements = { From 4765f2918d0efa7e453139cfe6af3070f630436f Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 23:29:14 +0300 Subject: [PATCH 09/17] update logic for no mic/camera cases --- client/pages/index.html | 2 +- client/scripts/common.js | 59 +++++++++- client/scripts/media.js | 234 ++++++++++++++++++++++++--------------- client/styles/media.css | 26 ++++- 4 files changed, 227 insertions(+), 94 deletions(-) diff --git a/client/pages/index.html b/client/pages/index.html index ba6b04d..5df7443 100644 --- a/client/pages/index.html +++ b/client/pages/index.html @@ -35,7 +35,7 @@

Локальный прокторинг

- +
diff --git a/client/scripts/common.js b/client/scripts/common.js index ed80490..6608e64 100644 --- a/client/scripts/common.js +++ b/client/scripts/common.js @@ -226,6 +226,63 @@ export async function showModalNotify(messages, title = "Уведомление" } } +export async function showModalConfirm(messages, title = "Подтверждение", showOnActiveTab = false, mediaIntependent = false) { + chrome.runtime.sendMessage({ action: "closePopup" }); + logClientAction({ action: "showModalConfirm", showOnActiveTab }); + + if (!Array.isArray(messages)) { + messages = [messages]; + } + + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ + action: "gotoMediaTab", + mediaExtensionUrl: chrome.runtime.getURL("pages/media.html") + }, (response) => { + if (chrome.runtime.lastError) { + console.error('Error send gotoMediaTab', chrome.runtime.lastError.message); + logClientAction({ action: "Error send gotoMediaTab", message: chrome.runtime.lastError.message }); + } else { + logClientAction({ action: "Response gotoMediaTab", response }); + } + }); + + const existingOverlay = document.getElementById('custom-modal-overlay'); + if (existingOverlay) existingOverlay.remove(); + + const overlay = document.createElement('div'); + overlay.id = 'custom-modal-overlay'; + + const modal = document.createElement('div'); + modal.id = 'custom-modal'; + + modal.innerHTML = ` +

${title}

+ + + `; + + const closeModal = (result) => { + overlay.remove(); + document.body.style.overflow = ''; + resolve(result); + }; + + modal.querySelector('#modal-confirm-yes').addEventListener('click', () => closeModal(true)); + modal.querySelector('#modal-confirm-no').addEventListener('click', () => closeModal(false)); + + document.body.style.overflow = 'hidden'; + overlay.appendChild(modal); + document.body.appendChild(overlay); + + }); +} + function sendModalNotifyToActiveTab(messages, title) { return new Promise((resolve, reject) => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { @@ -289,7 +346,7 @@ export function setReadyToUploadContainer(container, files) { const titleElement = document.createElement("div"); titleElement.id = "ready-to-upload-container-title"; - titleElement.innerHTML = `Файлов, доступных для выгрузки: ${files.length}`; + titleElement.innerHTML = `




Файлов, доступных для выгрузки: ${files.length}`; container.appendChild(titleElement); diff --git a/client/scripts/media.js b/client/scripts/media.js index da107eb..5f1e9e1 100644 --- a/client/scripts/media.js +++ b/client/scripts/media.js @@ -1,4 +1,4 @@ -import { showModalNotify, waitForNotificationSuppression, getBrowserFingerprint, generateObjectId, requestClearLogs, getDifferenceInTime, getFormattedDateString, getAvailableDiskSpace, saveBlobToFile } from './common.js'; +import { showModalNotify, showModalConfirm, waitForNotificationSuppression, getBrowserFingerprint, generateObjectId, requestClearLogs, getDifferenceInTime, getFormattedDateString, getAvailableDiskSpace, saveBlobToFile } from './common.js'; import { getCurrentDateString, setReadyToUploadContainer, parseDateString } from "./common.js"; import { logClientAction, flushLogs, checkAndCleanLogs, prepareLogs } from './logger.js'; @@ -21,6 +21,11 @@ var recorders = { camera: null }; +var noStreamConfirm = { + microphone: false, + camera: false +} + var readyToUploadContainer = document.querySelector('.ready-to-upload-container'); var combinedPreview = document.querySelector('.combined__preview'); @@ -85,10 +90,12 @@ class Metadata { const screenSettings = screenVideoTrack.getSettings(); this.metadata.screen.session_client_resolution = `${screenSettings.width}×${screenSettings.height}`; - this.metadata.camera.session_client_mime = recorders.camera.mimeType; - const [cameraVideoTrack] = streams.camera.getVideoTracks(); - const cameraSettings = cameraVideoTrack.getSettings(); - this.metadata.camera.session_client_resolution = `${cameraSettings.width}×${cameraSettings.height}`; + if (recorders.camera) { + this.metadata.camera.session_client_mime = recorders.camera.mimeType; + const [cameraVideoTrack] = streams.camera.getVideoTracks(); + const cameraSettings = cameraVideoTrack.getSettings(); + this.metadata.camera.session_client_resolution = `${cameraSettings.width}×${cameraSettings.height}`; + } } logClientAction({ action: "Set metadata record on" }); @@ -101,8 +108,10 @@ class Metadata { const screenFile = await combinedFileHandle.getFile(); this.metadata.screen.session_client_size = (screenFile.size / 1000000).toFixed(3); - const cameraFile = await cameraFileHandle.getFile(); - this.metadata.camera.session_client_size = (cameraFile.size / 1000000).toFixed(3); + if (cameraFileHandle){ + const cameraFile = await cameraFileHandle.getFile(); + this.metadata.camera.session_client_size = (cameraFile.size / 1000000).toFixed(3); + } logClientAction({ action: "Set metadata record off" }); }; @@ -130,6 +139,15 @@ const stopStreams = () => { logClientAction({ action: "Stop streams" }); }; +const stopStream = (stream) => { + const value = streams[stream]; + if (value) { + value.getTracks().forEach(track => track.stop()); + streams[stream] = null; + } + logClientAction({ action: `Stop stream ${stream}` }); +}; + async function checkOpenedPopup() { let a = await chrome.runtime.getContexts({contextTypes: ['POPUP']}); const isPopupOpen = a.length > 0; @@ -279,15 +297,23 @@ async function getMediaDevices() { if (micError.name === 'NotAllowedError') { micPermissionDenied = true; logClientAction({ action: "Microphone permission denied", error: "NotAllowedError" }); - await showModalNotify("Ошибка при доступе к микрофону: NotAllowedError", "Ошибка"); + //await showModalNotify("Ошибка при доступе к микрофону: NotAllowedError", "Ошибка"); } else { logClientAction({ action: "Microphone permission denied" }); - //alert('Ошибка при доступе к микрофону: ' + micError.message); - await showModalNotify('Ошибка при доступе к микрофону: ' + micError.message, "Ошибка"); + //await showModalNotify('Ошибка при доступе к микрофону: ' + micError.message, "Ошибка"); + } + let userAnswer = await showModalConfirm("При доступе к микрофону возникла ошибка, хотите продолжить без него?") + if (userAnswer) { + noStreamConfirm.microphone = true; + logClientAction({ action: "User've confirm work without a microphone" }) + await showModalNotify("Запись будет осуществлена без микрофона."); + stopStream('microphone'); + } + else { stopStreams(); reject(micError); - return; } + } if (!micPermissionDenied) { @@ -335,19 +361,27 @@ async function getMediaDevices() { } catch (camError) { if (camError.name === 'NotAllowedError') { logClientAction({ action: "Camera permission denied", error: "NotAllowedError" }); - await showModalNotify("Ошибка при доступе к камере: NotAllowedError", "Ошибка"); + //await showModalNotify("Ошибка при доступе к камере: NotAllowedError", "Ошибка"); camPermissionDenied = true; } else { logClientAction({ action: "Camera permission denied" }); - //alert('Ошибка при доступе к камере: ' + camError.message); - await showModalNotify('Ошибка при доступе к камере: ' + camError.message, "Ошибка"); + //await showModalNotify('Ошибка при доступе к камере: ' + camError.message, "Ошибка"); + } + + let userAnswer = await showModalConfirm("При доступе к камере возникла ошибка, хотите продолжить без неё?") + if (userAnswer) { + noStreamConfirm.camera = true; + logClientAction({ action: "User've confirm work without a camera" }) + await showModalNotify("Запись будет осуществлена без камеры."); + stopStream('camera'); + } + else { stopStreams(); reject(camError); - return; } } - if (micPermissionDenied || camPermissionDenied) { + if ((!noStreamConfirm.microphone && !noStreamConfirm.camera) && (micPermissionDenied || camPermissionDenied)) { stopStreams(); const extensionId = chrome.runtime.id; const settingsUrl = `chrome://settings/content/siteDetails?site=chrome-extension://${extensionId}`; @@ -373,26 +407,28 @@ async function getMediaDevices() { } // Обработка потери доступа - streams.camera.oninactive = async function () { - if (streamLossSource) return; - streamLossSource = 'camera'; - logClientAction('Camera stream inactive'); - - if (!recorders.combined && !recorders.camera) return; - - if (recorders.combined.state === 'inactive' && recorders.camera.state === 'inactive') { - await sendButtonsStates('needPermissions'); - await showModalNotify(["Разрешение на камеру отозвано.", - "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к камере потерян!"); - stopStreams(); - } else { - stopDuration(startTime); - await sendButtonsStates('failedUpload'); - await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к камере потерян!"); - // updateInvalidStopValue(true); - stopRecord(); - } - }; + if (streams.camera) { + streams.camera.oninactive = async function () { + if (streamLossSource) return; + streamLossSource = 'camera'; + logClientAction('Camera stream inactive'); + + if (!recorders.combined && !recorders.camera) return; + + if (recorders.combined.state === 'inactive' && recorders.camera.state === 'inactive') { + await sendButtonsStates('needPermissions'); + await showModalNotify(["Разрешение на камеру отозвано.", + "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к камере потерян!"); + stopStreams(); + } else { + stopDuration(startTime); + await sendButtonsStates('failedUpload'); + await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к камере потерян!"); + // updateInvalidStopValue(true); + stopRecord(); + } + }; + } streams.screen.getVideoTracks()[0].onended = async function () { chrome.runtime.sendMessage({ type: 'screenCaptureStatus', active: false }); @@ -414,29 +450,31 @@ async function getMediaDevices() { } }; - streams.microphone.getAudioTracks()[0].onended = async function () { - if (streamLossSource) return; - streamLossSource = 'microphone'; - logClientAction('Microphone stream ended'); - - if (!recorders.combined || recorders.combined.state === 'inactive') { - await sendButtonsStates('needPermissions'); - await showModalNotify(["Разрешение на микрофон отозвано.", - "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к микрофону потерян!"); - stopStreams(); - } else { - stopDuration(startTime); - await sendButtonsStates('failedUpload'); - await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения в расширении во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к микрофону потерян!"); - // updateInvalidStopValue(true); - stopRecord(); - } - }; + let combinedStreams = [streams.screen.getVideoTracks()[0]] + + if (streams.microphone) { + streams.microphone.getAudioTracks()[0].onended = async function () { + if (streamLossSource) return; + streamLossSource = 'microphone'; + logClientAction('Microphone stream ended'); + + if (!recorders.combined || recorders.combined.state === 'inactive') { + await sendButtonsStates('needPermissions'); + await showModalNotify(["Разрешение на микрофон отозвано.", + "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к микрофону потерян!"); + stopStreams(); + } else { + stopDuration(startTime); + await sendButtonsStates('failedUpload'); + await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения в расширении во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к микрофону потерян!"); + // updateInvalidStopValue(true); + stopRecord(); + } + }; + combinedStreams.push(streams.microphone.getAudioTracks()[0]) + } - streams.combined = new MediaStream([ - streams.screen.getVideoTracks()[0], - streams.microphone.getAudioTracks()[0] - ]); + streams.combined = new MediaStream(combinedStreams); combinedPreview.srcObject = streams.combined; cameraPreview.srcObject = streams.camera; @@ -451,7 +489,7 @@ async function getMediaDevices() { cameraPreview.height = 240; }; - cameraPreview.style.display = 'block'; + cameraPreview.style.display = streams.camera ? 'block' : 'none'; combinedPreview.style.display = 'block'; combinedPreview.muted = false; @@ -460,7 +498,7 @@ async function getMediaDevices() { logClientAction({ action: "Create combined recorder" }); - recorders.camera = createCameraMediaRecorder(); + recorders.camera = streams.camera ? createCameraMediaRecorder() : null; logClientAction({ action: "Create camera recorder" }); @@ -486,26 +524,27 @@ async function getMediaDevices() { } }; - recorders.camera.ondataavailable = async (event) => { - if (event.data.size > 0) { - const cameraWritableStream = await cameraFileHandle.createWritable({ keepExistingData: true }); + if (recorders.camera) + recorders.camera.ondataavailable = async (event) => { + if (event.data.size > 0) { + const cameraWritableStream = await cameraFileHandle.createWritable({ keepExistingData: true }); - const file = await cameraFileHandle.getFile(); - const size = file.size; + const file = await cameraFileHandle.getFile(); + const size = file.size; - console.log("cameraFileHandle", size); + console.log("cameraFileHandle", size); - await cameraWritableStream.write({ - type: "write", - position: size, - data: event.data - }); + await cameraWritableStream.write({ + type: "write", + position: size, + data: event.data + }); - await cameraWritableStream.close(); + await cameraWritableStream.close(); - await chrome.storage.local.set({"recording_end": (new Date()).toISOString()}); - } - }; + await chrome.storage.local.set({"recording_end": (new Date()).toISOString()}); + } + }; resolve(); } catch (error) { @@ -670,12 +709,13 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { logClientAction({ action: "Get media devices success" }); await sendButtonsStates('readyToRecord'); await showModalNotify(["Разрешения получены. Теперь вы можете начать запись.", + "Нажмите на кнопку «Начать запись» во всплывающем окне " + + "расширения прокторинга, когда будете готовы.", + "", "Для удобства уведомление о доступе к вашему экрану можно скрыть или передвинуть. НЕЛЬЗЯ НАЖИМАТЬ НА «Закрыть доступ».", "НЕЛЬЗЯ ОБНОВЛЯТЬ, ЗАКРЫВАТЬ СЛУЖЕБНУЮ ВКЛАДКУ во время записи! НЕЛЬЗЯ ЗАКРЫВАТЬ БРАУЗЕР во время записи!", "Предпросмотр будет отключен. Его можно включить по кнопке на служебной вкладке расширения. По умолчанию звук выключен и включается в плеере.", - "", - "Нажмите на кнопку «Начать запись» во всплывающем окне " + - "расширения прокторинга, когда будете готовы.",], +,], "Готово к записи"); }) .catch(async () => { @@ -731,6 +771,7 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (!suppress) { await showModalNotify( ["Запись экрана, микрофона и камеры началась. " + + "Во всплывающем окне разрешения доступна информация о времени начала записи и её продолжительности.", "Не отключайте разрешения этим элементам до окончания записи.", "Чтобы завершить запись, нажмите кнопку «Остановить запись» во всплывающем окне расширения прокторинга."], "Идёт запись", @@ -826,6 +867,8 @@ async function stopRecord() { endTime = new Date(); + console.log(metadata) + await metadata.setMetadatasRecordOff(endTime); let combinedFileSize = 0; @@ -882,7 +925,7 @@ async function stopRecord() { if (session_settings.server_connection || session_settings.local_video_saving) { await showModalNotify( - ["Для сохранения и отправки записи необходимо нажать кнопку «Отправить» во всплывающем окне расширения прокторинга."], + ["Для скачивания записи необходимо нажать кнопку «Сохранить» во всплывающем окне расширения прокторинга."], "Отправка записи", true ); @@ -906,9 +949,15 @@ async function startRecord() { logClientAction({ action: "Fail to start recording", reason: "Insufficient disk space" }); return; } - if (!combinedPreview.srcObject || !cameraPreview.srcObject) { - console.log('Выдайте разрешения'); - logClientAction({ action: "Fail to start recording", reason: "Missing media permissions" }); + if (!combinedPreview.srcObject) { + console.log('Выдайте разрешения к экрану'); + logClientAction({ action: "Fail to start recording", reason: "Missing media permission for screen" }); + return; + } + + if (!noStreamConfirm.camera && !cameraPreview.srcObject) { + console.log('Выдайте разрешения на камеру'); + logClientAction({ action: "Fail to start recording", reason: "Missing media permission for camera" }); return; } @@ -921,18 +970,23 @@ async function startRecord() { let startRecordTime = getCurrentDateString(startTime); combinedFileName = createScreenFileName(startRecordTime); - cameraFileName = createCameraFileName(startRecordTime); - await addFileToTempList(combinedFileName); - await addFileToTempList(cameraFileName); + + cameraFileName = null; + if (cameraPreview.srcObject) { + cameraFileName = createCameraFileName(startRecordTime); + await addFileToTempList(cameraFileName); + } try { combinedFileHandle = await rootDirectory.getFileHandle(combinedFileName, {create: true}); logClientAction({ action: "Create file handle", fileType: "screen", fileName: combinedFileName }); - cameraFileHandle = await rootDirectory.getFileHandle(cameraFileName, {create: true}); - logClientAction({ action: "Create file handle", fileType: "camera", fileName: cameraFileName }); - + cameraFileHandle = null; + if (cameraPreview.srcObject) { + cameraFileHandle = await rootDirectory.getFileHandle(cameraFileName, {create: true}); + logClientAction({ action: "Create file handle", fileType: "camera", fileName: cameraFileName }); + } logClientAction('Files added to temp list'); chrome.storage.local.set({ @@ -957,14 +1011,14 @@ async function startRecord() { // startTime = new Date(); recorders.combined.start(video_settings.video_saving_interval); - recorders.camera.start(video_settings.video_saving_interval); + if (recorders.camera) + recorders.camera.start(video_settings.video_saving_interval); isRecording = true; isPreviewEnabled = false; hideMutePreviews(); updatePreviewButton(); - console.log('Запись начата'); logClientAction('recording_started'); //chrome.runtime.sendMessage({ action: "closePopup" }); diff --git a/client/styles/media.css b/client/styles/media.css index 9fc4ab4..f70a89e 100644 --- a/client/styles/media.css +++ b/client/styles/media.css @@ -76,6 +76,28 @@ background: #0056b3; } +#modal-confirm-yes { + margin-top: 15px; + background: #008c05; + color: #faf0ec !important; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-weight: 700; +} + +#modal-confirm-no { + margin-top: 15px; + background: #8c0000; + color: #faf0ec !important; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-weight: 700; +} + .preview-label { font-size: 18px; @@ -214,8 +236,8 @@ left: 50%; transform: translateX(-50%); margin-top: 8px; - width: 24px; - height: 24px; + width: 48px; + height: 48px; z-index: 10; pointer-events: none; } From 6c7a120fc336aceb5b800ad1cb70ae98f48d6550 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 23:30:00 +0300 Subject: [PATCH 10/17] new version --- client/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/manifest.json b/client/manifest.json index 1097b62..e0c0988 100644 --- a/client/manifest.json +++ b/client/manifest.json @@ -1,7 +1,7 @@ { "name": "Local proctoring", "description": "Local recording tool containing screen, microphone and camera.", - "version": "2.0.0", + "version": "2.0.1", "manifest_version": 3, "permissions": [ "storage", From 7afa5223d8641527254504ee57727f0ed7838e2c Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 23:34:44 +0300 Subject: [PATCH 11/17] update tests (link -> work_title) --- tests/functional_test.py | 8 ++++---- tests/integration_test.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional_test.py b/tests/functional_test.py index 28cb4bd..0012cb0 100644 --- a/tests/functional_test.py +++ b/tests/functional_test.py @@ -84,14 +84,14 @@ def test_patronymic_field_validation(page: Page, extension_id: str) -> None: patronymic_input.fill("иванович") expect(patronymic_input).to_have_class(re.compile(r".*input-invalid.*")) -def test_link_field_validation(page: Page, extension_id: str) -> None: +def test_work_title_field_validation(page: Page, extension_id: str) -> None: page.goto(f"chrome-extension://{extension_id}/pages/index.html") - link_input = page.locator("#link_input") + work_title_input = page.locator("#work_title_input") - link_input.fill("https://se.moevm.info/doku.php/start") - expect(link_input).to_have_class(re.compile(r".*input-valid.*")) + work_title_input.fill("https://se.moevm.info/doku.php/start") + expect(work_title_input).to_have_class(re.compile(r".*input-valid.*")) def test_no_patronymic_checkbox_validation(page: Page, extension_id: str) -> None: page.goto(f"chrome-extension://{extension_id}/pages/index.html") diff --git a/tests/integration_test.py b/tests/integration_test.py index 40a0696..84c63d0 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -10,7 +10,7 @@ def check_integration(url): "surname": "Tester", "name": "Test", "patronymic": "Testerovich", - "link": "http://test.com" + "work_title": "http://test.com" } print(f"Создана форма {form_data} для интеграционного тестирования") From 94a889e5860395e9a6e5cd960038f5690e573c23 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 23:39:52 +0300 Subject: [PATCH 12/17] sync test w/new regexp --- tests/functional_test.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/functional_test.py b/tests/functional_test.py index 0012cb0..e762884 100644 --- a/tests/functional_test.py +++ b/tests/functional_test.py @@ -30,9 +30,15 @@ def test_name_field_validation(page: Page, extension_id: str) -> None: name_input.fill("Иван") expect(name_input).to_have_class(re.compile(r".*input-valid.*")) - name_input.fill("Albus-Severus") + name_input.fill("Альбус-Северус") + expect(name_input).to_have_class(re.compile(r".*input-valid.*")) + + name_input.fill("Альбус Северус") expect(name_input).to_have_class(re.compile(r".*input-valid.*")) + name_input.fill("Albus-Severus") + expect(name_input).to_have_class(re.compile(r".*input-invalid.*")) + name_input.fill("Дон Pedro") expect(name_input).to_have_class(re.compile(r".*input-invalid.*")) @@ -51,9 +57,15 @@ def test_surname_field_validation(page: Page, extension_id: str) -> None: surname_input.fill("Петров") expect(surname_input).to_have_class(re.compile(r".*input-valid.*")) - surname_input.fill("Smith-Johnson") + surname_input.fill("Петров-Иванов") expect(surname_input).to_have_class(re.compile(r".*input-valid.*")) + surname_input.fill("Петров Иванов") + expect(surname_input).to_have_class(re.compile(r".*input-valid.*")) + + surname_input.fill("Smith-Johnson") + expect(surname_input).to_have_class(re.compile(r".*input-invalid.*")) + surname_input.fill("Smith Johnson") expect(surname_input).to_have_class(re.compile(r".*input-invalid.*")) @@ -76,7 +88,7 @@ def test_patronymic_field_validation(page: Page, extension_id: str) -> None: expect(patronymic_input).to_have_class(re.compile(r".*input-valid.*")) patronymic_input.fill("Александровна Мария") - expect(patronymic_input).to_have_class(re.compile(r".*input-invalid.*")) + expect(patronymic_input).to_have_class(re.compile(r".*input-valid.*")) patronymic_input.fill("Иванович123") expect(patronymic_input).to_have_class(re.compile(r".*input-invalid.*")) From 0de38894fa5dfa16e2938eecad0cd5e6b75b5a10 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 16 Apr 2026 23:43:26 +0300 Subject: [PATCH 13/17] update release-archives --- .github/workflows/release_attacher.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_attacher.yml b/.github/workflows/release_attacher.yml index 951804a..311bd47 100644 --- a/.github/workflows/release_attacher.yml +++ b/.github/workflows/release_attacher.yml @@ -15,7 +15,7 @@ jobs: - name: Minimize client code run: | - apt install -y nodejs npm jq + sudo apt install -y nodejs npm jq npm install -g terser cd client for i in */*.js; do terser $i --compress -o $i; done From 60a46146d28ef175253bfafe2d84d82c4e953bfd Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 20 Jul 2026 16:52:03 +0300 Subject: [PATCH 14/17] update clien logic (settings, server-connection and etc) --- client/manifest.json | 5 ++++- client/pages/index.html | 4 ++-- client/scripts/index.js | 16 ++++++++-------- client/scripts/media.js | 16 ++++++++++++---- client/settings.json | 5 +++-- tests/functional_test.py | 8 ++++---- tests/integration_test.py | 2 +- 7 files changed, 34 insertions(+), 22 deletions(-) diff --git a/client/manifest.json b/client/manifest.json index e0c0988..91919e9 100644 --- a/client/manifest.json +++ b/client/manifest.json @@ -34,5 +34,8 @@ "16": "assets/icons/icon.png", "48": "assets/icons/icon.png", "128": "assets/icons/icon.png" - } + }, + "host_permissions": [ + "https://proctoring.moevm.pro/" + ] } diff --git a/client/pages/index.html b/client/pages/index.html index 5df7443..25c8c2e 100644 --- a/client/pages/index.html +++ b/client/pages/index.html @@ -24,8 +24,8 @@

Локальный прокторинг

Нет отчества
- - + +
diff --git a/client/scripts/index.js b/client/scripts/index.js index e65d599..83eb0db 100644 --- a/client/scripts/index.js +++ b/client/scripts/index.js @@ -24,7 +24,7 @@ const inputElements = { name: document.querySelector('#name_input'), surname: document.querySelector('#surname_input'), patronymic: document.querySelector('#patronymic_input'), - work_title: document.querySelector('#work_title_input') + title: document.querySelector('#title_input') }; const buttonElements = { @@ -93,7 +93,7 @@ const validationRules = { regex: validationStringRegExp, message: "Отчество должно начинаться с заглавной буквы и содержать только кириллицу и тире/пробел. Пример: 'Иванович'" }, - work_title: { + title: { regex: /.+/, message: "Название работы не должно быть пустым." } @@ -167,7 +167,7 @@ async function saveInputValues() { surname: inputElements.surname.value, patronymic: inputElements.patronymic.value, noPatronymicChecked: noPatronymicCheckbox.checked, - work_title: inputElements.work_title.value + title: inputElements.title.value } }); logClientAction({ action: "Save input values" }); @@ -476,7 +476,7 @@ async function startRecCallback() { name: inputElements.name.value, surname: inputElements.surname.value, patronymic: noPatronymicCheckbox.checked ? "Без_отчества" : inputElements.patronymic.value.trim(), - work_title: inputElements.work_title.value + title: inputElements.title.value }; chrome.runtime.sendMessage({ @@ -606,7 +606,7 @@ async function saveFormDataFilesLocally(formData, sessionId) { async function uploadFormDataToServer(formData, sessionId) { logClientAction({ action: "Send upload request", sessionId: sessionId, messageType: "upload_video" }); - const eventSource = new EventSource(`http://127.0.0.1:5000/progress/${sessionId}`); + const eventSource = new EventSource(`${session_settings.server_url}/progress/${sessionId}`); const steps = 7; @@ -636,7 +636,7 @@ async function uploadFormDataToServer(formData, sessionId) { }; try { - const response = await fetch('http://127.0.0.1:5000/upload_video', { + const response = await fetch(`${session_settings.server_url}/upload_video`, { method: "POST", body: formData, }); @@ -701,8 +701,8 @@ async function uploadVideo(files) { await chrome.storage.local.remove("metadata"); await chrome.storage.local.set({"session_status" : "need_init"}); - inputElements.work_title.value = ""; - inputElements.work_title.classList.remove('input-valid', 'input-invalid'); + inputElements.title.value = ""; + inputElements.title.classList.remove('input-valid', 'input-invalid'); await saveInputValues(); logClientAction("Clear work title field"); } diff --git a/client/scripts/media.js b/client/scripts/media.js index 5f1e9e1..01d426a 100644 --- a/client/scripts/media.js +++ b/client/scripts/media.js @@ -74,6 +74,10 @@ class Metadata { session_client_size: undefined // MB }, recording_sessions: [], + no_stream_confirm: { + microphone: false, + camera: false + } }; } @@ -95,7 +99,9 @@ class Metadata { const [cameraVideoTrack] = streams.camera.getVideoTracks(); const cameraSettings = cameraVideoTrack.getSettings(); this.metadata.camera.session_client_resolution = `${cameraSettings.width}×${cameraSettings.height}`; - } + } else + this.metadata.no_stream_confirm.microphone = noStreamConfirm.microphone; + this.metadata.no_stream_confirm.camera = noStreamConfirm.camera; } logClientAction({ action: "Set metadata record on" }); @@ -715,7 +721,7 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { "Для удобства уведомление о доступе к вашему экрану можно скрыть или передвинуть. НЕЛЬЗЯ НАЖИМАТЬ НА «Закрыть доступ».", "НЕЛЬЗЯ ОБНОВЛЯТЬ, ЗАКРЫВАТЬ СЛУЖЕБНУЮ ВКЛАДКУ во время записи! НЕЛЬЗЯ ЗАКРЫВАТЬ БРАУЗЕР во время записи!", "Предпросмотр будет отключен. Его можно включить по кнопке на служебной вкладке расширения. По умолчанию звук выключен и включается в плеере.", -,], + ], "Готово к записи"); }) .catch(async () => { @@ -733,7 +739,7 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { formData.append('name', message.formData.name || ''); formData.append('surname', message.formData.surname || ''); formData.append('patronymic', message.formData.patronymic || ''); - formData.append('work_title', message.formData.work_title || ''); + formData.append('title', message.formData.title || ''); function formDataToObject(formData) { const obj = {}; @@ -802,9 +808,10 @@ async function initSession(formData) { getBrowserFingerprint() try { - const response = await fetch('http://127.0.0.1:5000/start_session', { + const response = await fetch(`${session_settings.server_url}/start_session`, { method: 'POST', mode: 'cors', + credentials: 'include', body: formData }); @@ -869,6 +876,7 @@ async function stopRecord() { console.log(metadata) + // ВОТ ТУТ await metadata.setMetadatasRecordOff(endTime); let combinedFileSize = 0; diff --git a/client/settings.json b/client/settings.json index 5ad7c28..60865cd 100644 --- a/client/settings.json +++ b/client/settings.json @@ -20,7 +20,8 @@ "video_saving_interval": 5000 }, "session_settings" : { - "server_connection" : false, - "local_video_saving" : true + "server_connection" : true, + "local_video_saving" : false, + "server_url": "https://proctoring.moevm.pro/api/remoteSessions" } } diff --git a/tests/functional_test.py b/tests/functional_test.py index e762884..673ce56 100644 --- a/tests/functional_test.py +++ b/tests/functional_test.py @@ -96,14 +96,14 @@ def test_patronymic_field_validation(page: Page, extension_id: str) -> None: patronymic_input.fill("иванович") expect(patronymic_input).to_have_class(re.compile(r".*input-invalid.*")) -def test_work_title_field_validation(page: Page, extension_id: str) -> None: +def test_title_field_validation(page: Page, extension_id: str) -> None: page.goto(f"chrome-extension://{extension_id}/pages/index.html") - work_title_input = page.locator("#work_title_input") + title_input = page.locator("#title_input") - work_title_input.fill("https://se.moevm.info/doku.php/start") - expect(work_title_input).to_have_class(re.compile(r".*input-valid.*")) + title_input.fill("https://se.moevm.info/doku.php/start") + expect(title_input).to_have_class(re.compile(r".*input-valid.*")) def test_no_patronymic_checkbox_validation(page: Page, extension_id: str) -> None: page.goto(f"chrome-extension://{extension_id}/pages/index.html") diff --git a/tests/integration_test.py b/tests/integration_test.py index 84c63d0..071e7a0 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -10,7 +10,7 @@ def check_integration(url): "surname": "Tester", "name": "Test", "patronymic": "Testerovich", - "work_title": "http://test.com" + "title": "http://test.com" } print(f"Создана форма {form_data} для интеграционного тестирования") From a272356c343942103f07b92785ce33f73d46440f Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 20 Jul 2026 16:54:08 +0300 Subject: [PATCH 15/17] regardless server updates --- server/app.py | 17 +++++++++++------ server/static/filter.js | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/server/app.py b/server/app.py index 50c9a48..d84ad20 100644 --- a/server/app.py +++ b/server/app.py @@ -8,6 +8,8 @@ from bson import ObjectId from flask_cors import CORS from datetime import datetime, timezone +from logging import getLogger + app = Flask(__name__) CORS(app, resources={r"/*": {"origins": "*"}}) @@ -32,10 +34,10 @@ def start_session(): surname = data.get("surname") name = data.get("name") patronymic = data.get("patronymic") - work_title = data.get("work_title") + title = data.get("title") - if not (group and surname and name and patronymic and work_title): - return jsonify({"error": "Поля 'group', 'surname', 'name', 'patronymic', 'work_title' обязательны для заполнения"}), 400 + if not (group and surname and name and patronymic and title): + return jsonify({"error": f"Поля 'group', 'surname', 'name', 'patronymic', 'title' обязательны для заполнения. {data}"}), 400 session_start = datetime.now(timezone.utc) # Форматирование даты @@ -48,7 +50,7 @@ def start_session(): "surname": surname, "name": name, "patronymic": patronymic, - "work_title": work_title, + "title": title, "session_date_start": session_date_start, "session_time_start": session_time_start, "session_date_end": None, @@ -92,11 +94,14 @@ def convert_user_time_to_utc(user_time_str, user_time_offset): user_time_utc = user_time + user_time_offset return user_time_utc.strftime("%Y%m%dT%H%M%S") -@app.route("/upload_video", methods=["POST"]) +@app.route("/upload_video/", methods=["POST"]) def upload_video(): """Обрабатывает загрузку видео с клиента, сохраняет файл и обновляет данные сеанса.""" + getLogger().error(request.form) + getLogger().error(request.files) try: - if "screen_video" not in request.files or "camera_video" not in request.files or "id" not in request.form: + if "screen_video" not in request.files or "id" not in request.form: + getLogger('root').error(request.files) return jsonify({"error": "Отсутствует видеофайл или ID сессии"}), 400 id = request.form["id"] diff --git a/server/static/filter.js b/server/static/filter.js index 2a35b04..d8b6a45 100644 --- a/server/static/filter.js +++ b/server/static/filter.js @@ -44,7 +44,7 @@ document.addEventListener("DOMContentLoaded", function () { `${fields2column}`, "status", "metadata", - "work_title" + "title" ]; // Создаем заголовок таблицы From 8359bb5ec591ac97be920f8c22aa37d3b9e092dc Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 20 Jul 2026 16:55:53 +0300 Subject: [PATCH 16/17] update version --- client/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/manifest.json b/client/manifest.json index 91919e9..56b076c 100644 --- a/client/manifest.json +++ b/client/manifest.json @@ -1,7 +1,7 @@ { "name": "Local proctoring", "description": "Local recording tool containing screen, microphone and camera.", - "version": "2.0.1", + "version": "2.0.2", "manifest_version": 3, "permissions": [ "storage", From 3c607b85963eead556269dd849e92da7fad0b4ed Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 20 Jul 2026 18:02:57 +0300 Subject: [PATCH 17/17] Update settings.json --- client/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/settings.json b/client/settings.json index 60865cd..10ffaa3 100644 --- a/client/settings.json +++ b/client/settings.json @@ -21,7 +21,7 @@ }, "session_settings" : { "server_connection" : true, - "local_video_saving" : false, + "local_video_saving" : true, "server_url": "https://proctoring.moevm.pro/api/remoteSessions" } }