Бэкенд: сервер + data.json (законы, структура, видео-загрузка, жалобы, результаты тестов)

This commit is contained in:
aset 2026-08-10 10:08:00 +00:00
parent 12e74010c1
commit c7b78ec948
5 changed files with 336 additions and 71 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
data.json
uploads/
node_modules/
.vibe42-run.log
.vibe42-run.pid

82
package-lock.json generated Normal file
View File

@ -0,0 +1,82 @@
{
"name": "compliance-tv",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "compliance-tv",
"dependencies": {
"formidable": "^3.5.1"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"license": "ISC",
"dependencies": {
"asap": "^2.0.0",
"wrappy": "1"
}
},
"node_modules/formidable": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"license": "MIT",
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
"once": "^1.4.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"name": "compliance-tv",
"private": true,
"type": "commonjs",
"scripts": { "start": "node server.js" },
"dependencies": {
"formidable": "^3.5.1"
}
}

151
script.js
View File

@ -1,37 +1,6 @@
var LAWS = [
{ id: 1, title: "Қазақстан Республикасының «Сыбайлас жемқорлыққа қарсы іс-қимыл туралы» Заңы", cat: "Заң", desc: "Сыбайлас жемқорлыққа қарсы іс-қимылдың негізгі қағидаттары, тыйымдар мен жауапкершілік." },
{ id: 2, title: "ҚР «Мемлекеттік қызмет туралы» Заңы", cat: "Заң", desc: "Мемлекеттік қызметшілердің этикасы мен шектеулері, мүдделер қақтығысын реттеу." },
{ id: 3, title: "«Жеке деректер және оларды қорғау туралы» Заң", cat: "Заң", desc: "Жеке деректерді жинау, өңдеу және сақтау талаптары." },
{ id: 4, title: "Қазақстан Республикасының Азаматтық кодексі (құпия ақпарат бөлімі)", cat: "Құқық", desc: "Коммерциялық құпия және құпия ақпарат режимі." },
{ id: 5, title: "«Ақпаратқа қол жеткізу туралы» Заң", cat: "Заң", desc: "Ақпаратқа қол жеткізу тәртібі және қызметтік ақпараттың құпиялылығы." },
{ id: 6, title: "Телеканалдың Ішкі еңбек тәртібі", cat: "Ішкі регламент", desc: "Еңбек тәртібі, қауіпсіздік және жұмыс орнындағы әдеп нормалары." },
{ id: 7, title: "Қаржылық бақылау жөніндегі нұсқаулық", cat: "Ішкі регламент", desc: "Қаржылық операцияларды бақылау, сыйлықтар мен төлемдерді декларациялау тәртібі." },
{ id: 8, title: "Ақпараттық қауіпсіздік саясаты", cat: "Ішкі регламент", desc: "Ақпараттық ресурстарды пайдалану, пароль және құпия сөздермен жұмыс ережелері." },
{ id: 9, title: "«Ақшаны жылыстатуға қарсы іс-қимыл туралы» Заң", cat: "Заң", desc: "Күдікті операциялар туралы хабарлау және қаржылық мониторинг талаптары." },
{ id: 10, title: "Комплаенс-бақылау жөніндегі ереже", cat: "Ішкі регламент", desc: "Комплаенс-тексерулер, өзін-өзі тексеру және есептілік тәртібі." }
];
var STRUCTURE = [
{ id: 1, name: "Айгүл Сейтова", role: "Бас директор", dept: "Әкімшілік", head: true, emoji: "👩‍💼" },
{ id: 2, name: "Ерлан Қасымов", role: "Директордың орынбасары", dept: "Әкімшілік", head: false, emoji: "👨‍💼" },
{ id: 3, name: "Асет Бисенов", role: "Комплаенс офицер", dept: "Комплаенс қызметі", head: false, emoji: "🛡️" },
{ id: 4, name: "Гүлназ Омарова", role: "Заңгер", dept: "Құқықтық департамент", head: false, emoji: "⚖️" },
{ id: 5, name: "Дәурен Нұрлан", role: "Бас бухгалтер", dept: "Қаржы департаменті", head: false, emoji: "💼" },
{ id: 6, name: "Майра Жұмабек", role: "HR директоры", dept: "Кадр департаменті", head: false, emoji: "🧑‍🏫" },
{ id: 7, name: "Бауыржан Серік", role: "IT директоры", dept: "IT департаменті", head: false, emoji: "💻" },
{ id: 8, name: "Ляззат Төлеген", role: "Бас редактор", dept: "Редакция", head: false, emoji: "📰" }
];
var VIDEOS = [
{ id: 1, title: "Комплаенс деген не?", category: "Кіріспе", duration: "4:32", desc: "Комплаенс ұғымы және телеканалдағы рөлі" },
{ id: 2, title: "Мүдделер қақтығысы: қалай анықтауға?", category: "Этика", duration: "3:45", desc: "Мүдделер қақтығысының белгілері мен шешімдері" },
{ id: 3, title: "Сыбайлас жемқорлыққа қарсы саясат", category: "Сыбайлас жемқорлық", duration: "5:18", desc: "Негізгі тыйымдар мен жауапкершілік" },
{ id: 4, title: "Сыйлықтар мен төлемдер: шегі қандай?", category: "Сыбайлас жемқорлық", duration: "3:20", desc: "Сыйлық қабылдау ережелері және декларациялау" },
{ id: 5, title: "Құпия ақпаратпен жұмыс", category: "Ақпарат", duration: "4:55", desc: "Құпия ақпаратты қорғау ережелері" },
{ id: 6, title: "Жеке деректерді қорғау", category: "Ақпарат", duration: "3:55", desc: "Жеке деректермен жұмыс істеу талаптары" },
{ id: 7, title: "Қысым мен кемсітушілік", category: "Этика", duration: "4:28", desc: "Жұмыс орнындағы әдеп және теңдік" },
{ id: 8, title: "Әлеуметтік желілердегі мінез-құлық", category: "Этика", duration: "3:38", desc: "Қызметкердің әлеуметтік желідегі әдебі" }
];
var LAWS = [];
var STRUCTURE = [];
var VIDEOS = [];
var QUESTIONS = [
{ q: "Сыйлықты декларациясыз қабылдауға болатын ең жоғары шекті мөлшері:", options: ["5 АЕК", "10 АЕК", "20 АЕК", "Кез келген"], correct: 0 },
@ -52,14 +21,7 @@ var testTaken = false;
var userName = "";
var userDept = "";
var mockResults = [
{ name: "Айгүл Сейтова", dept: "Әкімшілік", score: 10, total: 10, date: "2026-08-10" },
{ name: "Дәурен Нұрлан", dept: "Қаржы", score: 9, total: 10, date: "2026-08-10" },
{ name: "Майра Жұмабек", dept: "Кадр", score: 8, total: 10, date: "2026-08-09" },
{ name: "Бауыржан Серік", dept: "IT", score: 7, total: 10, date: "2026-08-09" },
{ name: "Ляззат Төлеген", dept: "Редакция", score: 6, total: 10, date: "2026-08-08" }
];
var mockResults = [];
var results = [];
var resultsBackendReady = false;
@ -67,6 +29,19 @@ function getResults() {
return resultsBackendReady ? results : mockResults;
}
function loadData() {
Promise.all([
fetch("/api/laws").then(function (r) { return r.json(); }).then(function (d) { LAWS = d; }),
fetch("/api/structure").then(function (r) { return r.json(); }).then(function (d) { STRUCTURE = d; }),
fetch("/api/videos").then(function (r) { return r.json(); }).then(function (d) { VIDEOS = d; })
]).then(function () {
renderLawChips();
renderLaws();
renderStructure();
renderVideos();
}).catch(function () {});
}
function renderLaws() {
var query = (document.getElementById("law-search").value || "").toLowerCase();
var activeCat = document.querySelector(".chip.active");
@ -122,9 +97,13 @@ function renderStructure() {
function renderVideos() {
var grid = document.getElementById("video-grid");
if (!VIDEOS.length) {
grid.innerHTML = '<div class="empty">Әзірге видео жоқ — офицер жүктеген кезде пайда болады</div>';
return;
}
grid.innerHTML = VIDEOS.map(function (v) {
return '<div class="video-card" onclick="openVideo(' + v.id + ')">' +
'<div class="video-thumbnail">' + v.desc + '</div>' +
'<div class="video-thumbnail">' + (v.desc || "") + '</div>' +
'<div class="video-info">' +
'<div class="video-title">' + v.title + '</div>' +
'<div class="video-meta"><span>' + v.category + '</span><span class="video-duration">' + v.duration + '</span></div>' +
@ -134,7 +113,18 @@ function renderVideos() {
function openVideo(id) {
var v = VIDEOS.find(function (x) { return x.id === id; });
if (v) alert("🎬 «" + v.title + "»\n\nРеалды нұсқада мұнда видео ойнайды (файлды офицер жүктеген кезде).");
if (!v) return;
if (v.src) {
var overlay = document.createElement("div");
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:999;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px";
overlay.innerHTML = '<video src="' + v.src + '" controls autoplay style="max-width:90vw;max-height:80vh;border-radius:10px"></video>' +
'<button style="padding:10px 24px;border:none;border-radius:8px;cursor:pointer;font-size:14px">Жабу (X)</button>';
overlay.querySelector("button").addEventListener("click", function () { overlay.remove(); });
overlay.addEventListener("click", function (e) { if (e.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
} else {
alert("🎬 «" + v.title + "»\n\nРеалды нұсқада мұнда видео ойнайды (файлды офицер жүктеген кезде).");
}
}
function initTest() {
@ -208,17 +198,20 @@ function finishTest() {
}
function saveResult(name, dept, sc, total) {
var rec = { name: name, dept: dept, score: sc, total: total, date: new Date().toISOString().slice(0, 10) };
if (resultsBackendReady) {
fetch("/api/results", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(rec)
}).catch(function () {});
} else {
results.push(rec);
renderDashboard();
}
var rec = { name: name, dept: dept, score: sc, total: total };
fetch("/api/results", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(rec)
}).then(function () {
return fetch("/api/results").then(function (r) { return r.json(); });
}).then(function (d) {
if (Array.isArray(d)) {
results = d;
resultsBackendReady = true;
renderDashboard();
}
}).catch(function () {});
}
function renderStats() {
@ -308,19 +301,17 @@ document.getElementById("complaint-form").addEventListener("submit", function (e
category: form.category.value,
text: form.text.value
};
if (resultsBackendReady) {
fetch("/api/complaints", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
}).then(function () {
form.reset();
showFormSuccess();
}).catch(function () { form.reset(); showFormSuccess(); });
} else {
fetch("/api/complaints", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
}).then(function () {
form.reset();
showFormSuccess();
}
}).catch(function () {
form.reset();
showFormSuccess();
});
});
function showFormSuccess() {
@ -332,7 +323,28 @@ function showFormSuccess() {
document.getElementById("video-form").addEventListener("submit", function (e) {
e.preventDefault();
alert("📤 Бұл мүмкіндік сервер қосылғанда жұмыс істейді (Рецепт: видео жүктеу).");
var form = e.target;
var btn = form.querySelector("button");
var file = form.file.files[0];
if (!file) { alert("Видео файлды таңдаңыз"); return; }
var fd = new FormData();
fd.append("title", form.title.value);
fd.append("file", file);
btn.disabled = true;
btn.textContent = "Жүктелуде...";
fetch("/api/upload", { method: "POST", body: fd })
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.ok) {
alert("✅ Видео жүктелді!");
form.reset();
loadData();
} else {
alert("Қате: файл жүктелмеді");
}
})
.catch(function () { alert("Сервер қолжетімсіз"); })
.finally(function () { btn.disabled = false; btn.textContent = "Жүктеу"; });
});
document.querySelectorAll(".nav-link").forEach(function (link) {
@ -344,9 +356,6 @@ document.querySelectorAll(".nav-link").forEach(function (link) {
});
});
renderLawChips();
renderLaws();
renderStructure();
renderVideos();
renderDashboard();
loadData();
tryBackend();

160
server.js Normal file
View File

@ -0,0 +1,160 @@
const http = require("http");
const fs = require("fs");
const path = require("path");
const formidable = require("formidable").default || require("formidable");
const PORT = process.env.PORT || 3000;
const ROOT = __dirname;
const DATA = path.join(ROOT, "data.json");
const UPLOADS = path.join(ROOT, "uploads");
const QUESTIONS_TOTAL = 10;
if (!fs.existsSync(UPLOADS)) fs.mkdirSync(UPLOADS);
const EMPTY = { laws: [], structure: [], videos: [], complaints: [], results: [] };
const SEED = {
laws: [
{ id: 1, title: "Қазақстан Республикасының «Сыбайлас жемқорлыққа қарсы іс-қимыл туралы» Заңы", cat: "Заң", desc: "Сыбайлас жемқорлыққа қарсы іс-қимылдың негізгі қағидаттары, тыйымдар мен жауапкершілік." },
{ id: 2, title: "ҚР «Мемлекеттік қызмет туралы» Заңы", cat: "Заң", desc: "Мемлекеттік қызметшілердің этикасы мен шектеулері, мүдделер қақтығысын реттеу." },
{ id: 3, title: "«Жеке деректер және оларды қорғау туралы» Заң", cat: "Заң", desc: "Жеке деректерді жинау, өңдеу және сақтау талаптары." },
{ id: 4, title: "Қазақстан Республикасының Азаматтық кодексі (құпия ақпарат бөлімі)", cat: "Құқық", desc: "Коммерциялық құпия және құпия ақпарат режимі." },
{ id: 5, title: "«Ақпаратқа қол жеткізу туралы» Заң", cat: "Заң", desc: "Ақпаратқа қол жеткізу тәртібі және қызметтік ақпараттың құпиялылығы." },
{ id: 6, title: "Телеканалдың Ішкі еңбек тәртібі", cat: "Ішкі регламент", desc: "Еңбек тәртібі, қауіпсіздік және жұмыс орнындағы әдеп нормалары." },
{ id: 7, title: "Қаржылық бақылау жөніндегі нұсқаулық", cat: "Ішкі регламент", desc: "Қаржылық операцияларды бақылау, сыйлықтар мен төлемдерді декларациялау тәртібі." },
{ id: 8, title: "Ақпараттық қауіпсіздік саясаты", cat: "Ішкі регламент", desc: "Ақпараттық ресурстарды пайдалану, пароль және құпия сөздермен жұмыс ережелері." },
{ id: 9, title: "«Ақшаны жылыстатуға қарсы іс-қимыл туралы» Заң", cat: "Заң", desc: "Күдікті операциялар туралы хабарлау және қаржылық мониторинг талаптары." },
{ id: 10, title: "Комплаенс-бақылау жөніндегі ереже", cat: "Ішкі регламент", desc: "Комплаенс-тексерулер, өзін-өзі тексеру және есептілік тәртібі." }
],
structure: [
{ id: 1, name: "Айгүл Сейтова", role: "Бас директор", dept: "Әкімшілік", head: true, emoji: "👩‍💼" },
{ id: 2, name: "Ерлан Қасымов", role: "Директордың орынбасары", dept: "Әкімшілік", head: false, emoji: "👨‍💼" },
{ id: 3, name: "Асет Бисенов", role: "Комплаенс офицер", dept: "Комплаенс қызметі", head: false, emoji: "🛡️" },
{ id: 4, name: "Гүлназ Омарова", role: "Заңгер", dept: "Құқықтық департамент", head: false, emoji: "⚖️" },
{ id: 5, name: "Дәурен Нұрлан", role: "Бас бухгалтер", dept: "Қаржы департаменті", head: false, emoji: "💼" },
{ id: 6, name: "Майра Жұмабек", role: "HR директоры", dept: "Кадр департаменті", head: false, emoji: "🧑‍🏫" },
{ id: 7, name: "Бауыржан Серік", role: "IT директоры", dept: "IT департаменті", head: false, emoji: "💻" },
{ id: 8, name: "Ляззат Төлеген", role: "Бас редактор", dept: "Редакция", head: false, emoji: "📰" }
],
videos: [
{ id: 1, title: "Комплаенс деген не?", category: "Кіріспе", duration: "4:32", desc: "Комплаенс ұғымы және телеканалдағы рөлі" },
{ id: 2, title: "Мүдделер қақтығысы: қалай анықтауға?", category: "Этика", duration: "3:45", desc: "Мүдделер қақтығысының белгілері мен шешімдері" },
{ id: 3, title: "Сыбайлас жемқорлыққа қарсы саясат", category: "Сыбайлас жемқорлық", duration: "5:18", desc: "Негізгі тыйымдар мен жауапкершілік" },
{ id: 4, title: "Сыйлықтар мен төлемдер: шегі қандай?", category: "Сыбайлас жемқорлық", duration: "3:20", desc: "Сыйлық қабылдау ережелері және декларациялау" },
{ id: 5, title: "Құпия ақпаратпен жұмыс", category: "Ақпарат", duration: "4:55", desc: "Құпия ақпаратты қорғау ережелері" },
{ id: 6, title: "Жеке деректерді қорғау", category: "Ақпарат", duration: "3:55", desc: "Жеке деректермен жұмыс істеу талаптары" },
{ id: 7, title: "Қысым мен кемсітушілік", category: "Этика", duration: "4:28", desc: "Жұмыс орнындағы әдеп және теңдік" },
{ id: 8, title: "Әлеуметтік желілердегі мінез-құлық", category: "Этика", duration: "3:38", desc: "Қызметкердің әлеуметтік желідегі әдебі" }
],
complaints: [],
results: []
};
function load() {
let d;
try { d = JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { d = null; }
if (!d) {
save(JSON.parse(JSON.stringify(SEED)));
return JSON.parse(JSON.stringify(SEED));
}
["laws", "structure", "videos", "complaints", "results"].forEach(function (k) {
if (!Array.isArray(d[k])) d[k] = [];
});
return d;
}
function save(d) {
fs.writeFileSync(DATA, JSON.stringify(d, null, 2));
}
function body(req) {
return new Promise((resolve) => {
let s = "";
req.on("data", (c) => (s += c));
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
});
}
function json(res, code, obj) {
res.writeHead(code, { "Content-Type": "application/json" });
res.end(JSON.stringify(obj));
}
http.createServer(async (req, res) => {
const url = req.url.split("?")[0];
// ===== Законы и материалы =====
if (req.method === "GET" && url === "/api/laws") return json(res, 200, load().laws);
if (req.method === "POST" && url === "/api/laws") {
const d = load(); const b = await body(req);
d.laws.push({ id: Date.now(), cat: "Ішкі регламент", ...b });
save(d); return json(res, 200, { ok: true });
}
// ===== Структура =====
if (req.method === "GET" && url === "/api/structure") return json(res, 200, load().structure);
// ===== Видео =====
if (req.method === "GET" && url === "/api/videos") return json(res, 200, load().videos);
if (req.method === "POST" && url === "/api/upload") {
const form = formidable({ uploadDir: UPLOADS, keepExtensions: true, maxFileSize: 200 * 1024 * 1024 });
form.parse(req, (err, fields, files) => {
const f = files.file && files.file[0];
if (err || !f) return json(res, 400, { error: "file" });
const rel = "uploads/" + path.basename(f.filepath);
const d = load();
d.videos.push({
id: Date.now(),
title: (fields.title && fields.title[0]) || "Видео сабақ",
category: "Жаңа сабақ",
duration: "—",
desc: "Жаңадан қосылған видео сабақ",
src: "/" + rel
});
save(d);
return json(res, 200, { ok: true, src: "/" + rel });
});
return;
}
// ===== Жалобы =====
if (req.method === "GET" && url === "/api/complaints") return json(res, 200, load().complaints);
if (req.method === "POST" && url === "/api/complaints") {
const d = load(); const b = await body(req);
d.complaints.push({
id: Date.now(),
category: b.category || "Басқа",
text: b.text || "",
date: new Date().toISOString().slice(0, 10),
status: "жаңа"
});
save(d); return json(res, 200, { ok: true });
}
// ===== Результаты тестов =====
if (req.method === "GET" && url === "/api/results") return json(res, 200, load().results);
if (req.method === "POST" && url === "/api/results") {
const d = load(); const b = await body(req);
d.results.push({
name: b.name || "Аноним",
dept: b.dept || "—",
score: Number(b.score) || 0,
total: Number(b.total) || QUESTIONS_TOTAL,
date: new Date().toISOString().slice(0, 10)
});
save(d); return json(res, 200, { ok: true });
}
// ===== Статика =====
let file = url === "/" ? "/index.html" : url;
const full = path.join(ROOT, file);
if (full.startsWith(ROOT) && fs.existsSync(full) && fs.statSync(full).isFile()) {
const ext = path.extname(full);
const type =
ext === ".css" ? "text/css" :
ext === ".js" ? "application/javascript" :
ext === ".mp4" ? "video/mp4" :
ext === ".webm" ? "video/webm" :
ext === ".mov" ? "video/quicktime" : "text/html";
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
return res.end(fs.readFileSync(full));
}
json(res, 404, { error: "Not found" });
}).listen(PORT, () => console.log("Compliance TV сервер: порт " + PORT));