diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93d5a9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +data.json +uploads/ +node_modules/ +.vibe42-run.log +.vibe42-run.pid diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4b9c45d --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..78a32c6 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "compliance-tv", + "private": true, + "type": "commonjs", + "scripts": { "start": "node server.js" }, + "dependencies": { + "formidable": "^3.5.1" + } +} diff --git a/script.js b/script.js index 50d643a..523e4ae 100644 --- a/script.js +++ b/script.js @@ -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 = '
Әзірге видео жоқ — офицер жүктеген кезде пайда болады
'; + return; + } grid.innerHTML = VIDEOS.map(function (v) { return '
' + - '
' + v.desc + '
' + + '
' + (v.desc || "") + '
' + '
' + '
' + v.title + '
' + '
' + v.category + '' + v.duration + '
' + @@ -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 = '' + + ''; + 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(); diff --git a/server.js b/server.js new file mode 100644 index 0000000..e17916e --- /dev/null +++ b/server.js @@ -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));