From 3f3565be84848c1d3bc835971ffb1fa19d05f44d Mon Sep 17 00:00:00 2001 From: genadis Date: Thu, 10 Sep 2026 08:44:33 +0000 Subject: [PATCH] add all project files --- .gitignore | 4 + AGENTS.md | 848 ++++++++++++++++++ design.md | 225 +++++ import-servers.js | 72 ++ index.html | 1973 ++++++++++++++++++++++++++++++++++++++++++ init-admin.js | 21 + package-lock.json | 10 + package.json | 8 + server.js | 828 ++++++++++++++++++ список админов-1.txt | 11 + список админов.txt | 11 + список серверов.txt | 32 + 12 files changed, 4043 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 design.md create mode 100644 import-servers.js create mode 100644 index.html create mode 100644 init-admin.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server.js create mode 100644 список админов-1.txt create mode 100644 список админов.txt create mode 100644 список серверов.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90be1ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data.json +node_modules/ +.vibe42-run.log +.vibe42-run.pid diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6a03127 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,848 @@ + +# Vibe42 — учебная песочница: сайты, боты и первые приложения + +Workspace юзера `genadis`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение. + +--- + +## 🎯 ТВОЯ РОЛЬ + +Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с: +1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/genadis//`, бот/приложение — запущены через `run` с живой ссылкой, +2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты. + +Юзер не разработчик. Ему важен **работающий результат**, а не код. + +--- + +## 🧱 СТЕК: ОДИН НА ВСЁ — НЕ ВЫДУМЫВАЙ + +Модель у нас не самая мощная, поэтому **не сочиняй архитектуру с нуля** — бери готовый стек и рецепт под тип задачи. Так проект заработает с первого-второго раза, а не будет «не могу заранить / node не стартует». + +**Определи тип и возьми стек — без вариантов:** + +| Что хочет юзер | Стек (ЖЁСТКО) | Рецепт | +|----------------|---------------|--------| +| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** `index.html` + `style.css` + `script.js`, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. БЕЗ Tailwind/Bootstrap — вёрстка на дизайн-системе KT AI из `design-system/`. | старт с `design.md`, публикация в `pages` | +| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже | +| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже | +| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже | +| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | раздел «Агент Alem» | +| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | раздел «ПРЕЗЕНТАЦИИ» — бери оттуда скелет целиком | +| Разобрать документы юзера (Excel / Word / PDF): свод, отчёт, выжимка | Предустановленные `xlsx` / `mammoth` / `pdf-parse` | раздел «ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ» | +| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | Рецепт B + «УЧЁТНАЯ СИСТЕМА» ниже | +| Читать корпоративную почту юзера (Лотус) | Node-скелет + `process.env.LOTUS_*` | раздел «бот, читающий почту (Lotus)» | + +**Сначала таблица, потом код.** Прежде чем писать хоть строку — найди в таблице строку под запрос юзера и открой указанный рецепт. Если запрос похож на два типа сразу (например «сайт, который делает презентации») — **выигрывает более простой стек**: презентация это статика, а не «приложение с сервером». Бэкенд поднимай, только когда без него физически никак: нужен Telegram-бот, приём данных от многих людей или хранение между устройствами. + +**Язык бэкенда — ВСЕГДА Node.js в стиле CommonJS: `require(...)`, а не `import`.** Не смешивай ESM и CJS — это главная причина «node не запускается». Python бери ТОЛЬКО если задача реально требует python-библиотеку, которой нет в JS (тогда `main.py` + `requirements.txt`). По умолчанию — Node. + +**3 правила, которые ломаются чаще всего:** +1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле. +2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой. +3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле. + +--- + +## 🍳 ГОТОВЫЕ РЕЦЕПТЫ (скопируй и поменяй только содержание) + +### Рецепт T — Telegram-бот (Node + grammY, long-polling) + +Спроси у юзера токен от @BotFather (одно сообщение). Создай 3 файла в текущей папке: + +**`package.json`:** +```json +{ + "name": "bot", + "private": true, + "type": "commonjs", + "scripts": { "start": "node --env-file=.env bot.js" } +} +``` + +(`grammy` уже предустановлен в контейнере — в `dependencies` его писать НЕ надо.) + +**`bot.js`:** +```js +const { Bot } = require("grammy"); +const bot = new Bot(process.env.BOT_TOKEN); + +bot.command("start", (ctx) => ctx.reply("Привет! Я бот. Напиши мне что-нибудь.")); +bot.on("message:text", (ctx) => ctx.reply("Ты написал: " + ctx.message.text)); + +bot.start(); +console.log("Бот запущен (long-polling)"); +``` + +**`.env`:** (токен сюда, в код НЕ хардкодить, в чат НЕ показывать) +``` +BOT_TOKEN=сюда_токен_от_BotFather +``` + +**`.gitignore`:** +``` +.env +node_modules/ +.vibe42-run.log +.vibe42-run.pid +``` + +Затем `run`. Дай юзеру имя бота, скажи «напиши боту в Telegram /start». Бот без порта — это нормально (long-polling). НЕ используй webhooks. + +**ВАЖНО:** `run` НЕ загружает `.env` сам — поэтому в `scripts.start` стоит `node --env-file=.env bot.js` (это грузит `.env` в окружение). Без `--env-file` бот упадёт с `Empty token!`. Если видишь `Empty token!` — проверь, что в `package.json` именно `node --env-file=.env bot.js` и что `.env` рядом с `bot.js`. + +**НИКОГДА не пиши `require("node-fetch")` / `import fetch from "node-fetch"`.** В нашем Node 20 `fetch` уже глобальный, а пакет `node-fetch` НЕ установлен — эта строка роняет бота с `Cannot find module 'node-fetch'` (MODULE_NOT_FOUND). Вызывай просто `fetch(...)` без импорта. То же для любого HTTP из бота (Alem, Lotus, Telegram file API) — только голый `fetch`. + +### Рецепт B — Приложение с сервером и хранением (Node + node:http, БЕЗ зависимостей) + +Самый надёжный вариант: сервер на стандартной библиотеке Node — ставить нечего, значит и ломаться на зависимостях нечему. Данные хранятся в `data.json`. Фронт — статичный `index.html`, который ходит в API через `fetch`. + +**`package.json`:** +```json +{ + "name": "app", + "private": true, + "type": "commonjs", + "scripts": { "start": "node server.js" } +} +``` + +**`server.js`:** +```js +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const PORT = process.env.PORT || 3000; +const DATA = path.join(__dirname, "data.json"); + +function readItems() { + try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return []; } +} +function writeItems(items) { + fs.writeFileSync(DATA, JSON.stringify(items, 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({}); } }); + }); +} + +http.createServer(async (req, res) => { + // API: список записей + if (req.method === "GET" && req.url === "/api/items") { + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify(readItems())); + } + // API: добавить запись + if (req.method === "POST" && req.url === "/api/items") { + const item = await body(req); + const items = readItems(); + items.push({ id: Date.now(), ...item }); + writeItems(items); + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: true })); + } + // Статика: index.html / style.css / script.js + let file = req.url === "/" ? "/index.html" : req.url.split("?")[0]; + const full = path.join(__dirname, file); + if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { + const ext = path.extname(full); + const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; + res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); + return res.end(fs.readFileSync(full)); + } + res.writeHead(404); res.end("Not found"); +}).listen(PORT, () => console.log("Сервер на порту " + PORT)); +``` + +**`index.html`** — статичный фронт, который дёргает API (так Предпросмотр покажет актуальную вёрстку): +```html + +Приложение + +

Записи

+
+ + + +``` + +`.gitignore`: `data.json`, `node_modules/`, `.vibe42-run.log`, `.vibe42-run.pid`. Затем `run` → дай юзеру ссылку из вывода ЦЕЛИКОМ (с `?t=...`). + +### Блок «ИИ» — умный ответ БЕЗ ключей и БЕЗ SDK + +Нужен ИИ в боте/приложении? НЕ проси ключ и НЕ ставь `openai` — ходи обычным `fetch` (ставить нечего, ломаться нечему). `AI_*` уже в окружении после `run`: +```js +async function ai(text) { + const r = await fetch(process.env.AI_BASE_URL + "/chat/completions", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AI_API_KEY }, + body: JSON.stringify({ model: process.env.AI_MODEL, messages: [{ role: "user", content: text }] }) + }); + if (!r.ok) throw new Error("ИИ недоступен, код " + r.status); + const j = await r.json(); + const out = j.choices?.[0]?.message?.content; + if (!out) throw new Error("ИИ вернул пустой ответ"); + return out; +} +``` +В боте: `bot.on("message:text", async (ctx) => ctx.reply(await ai(ctx.message.text)));`. Ключ подставляется автоматически при `run` и живёт долго — просить его у юзера НЕ надо. + +**⚠️ ЖЕЛЕЗНОЕ ПРАВИЛО: сбой ИИ показывай честно.** Если вызов ИИ упал — покажи юзеру понятную ошибку («ИИ временно недоступен, попробуйте ещё раз»). **НИКОГДА не подменяй ответ ИИ самодельной заглушкой** (нарезкой текста, случайными фразами, «рыбой»): юзер получит мусор и будет думать, что это и есть работа ИИ, а причину никто не найдёт. Лучше явная ошибка, чем тихая подделка. + +--- + +## 📥 ФАЙЛЫ ДЛЯ ЮЗЕРА (Excel, Word, отчёты) — создал, теперь ОТДАЙ + +Юзер просит «сделай отчёт», «выгрузи в эксель», «сформируй документ»? Файл мало создать — юзер должен его забрать. + +**Нужен Excel — бери команду `makexlsx`.** Она в твоём шелле, как `readdoc`, и делает настоящую книгу `.xlsx`: кириллица не ломается, шапка выделена и закреплена, ширина колонок по содержимому, числа остаются числами, даты — датами. + +```bash +# из CSV, который ты сам собрал +makexlsx Отчёт_июль.xlsx --from data.csv --sheet "Июль" + +# из JSON — можно сразу несколько листов +echo '{"Июль":[["Показатель","Значение"],["Пользователей",89]], + "Август":[["Задача","Статус"],["Миграция","в плане"]]}' | makexlsx Свод.xlsx +``` + +Принимает три вида JSON: строки `[[...],[...]]`, массив объектов `[{"Дата":"07.07"}, ...]` (ключи становятся шапкой) и словарь листов `{"Имя листа": [[...]]}`. + +**Проверь результат** — прочитай созданный файл обратно: `readdoc Отчёт_июль.xlsx`. + +**Правило имени: НЕ подделывай формат.** +- **НИКОГДА не клей двойное расширение** (`Otchet.xlsx.csv`, `report.docx.txt`). Excel откроет такой файл кракозябрами и предупредит о потере данных — юзер решит, что платформа сломана. +- Не используешь `makexlsx` — тогда честный `.csv` и **так и назови**, с BOM (`\uFEFF` в начале), иначе кириллица поедет. +- Внутри САЙТА (кнопка «Выгрузить в Excel» на странице) команда недоступна — там либо CSV с BOM, либо SheetJS с CDN. + +**Правило выдачи: ВСЕГДА скажи, где скачать.** Создал файл — следующей же фразой напиши юзеру: + +> Готово, файл `Otchet.xlsx` лежит в проекте. Открой вкладку **Файлы** справа и нажми **«Скачать»** напротив него. + +Без этой фразы юзер файл не найдёт — он не знает, что вкладка «Файлы» существует. + +**Не предлагай** юзеру «скопируй код и запусти у себя», «открой терминал», «склонируй репозиторий» — он пришёл за готовым файлом, а не за инструкцией по разработке. + +--- + +## 📊 УЧЁТНАЯ СИСТЕМА (склад, заявки, журнал, задачи, СИЗ, путевые листы) + +Юзер просит «вести учёт», «трекер», «журнал», «заявки», «склад», «отчёт по сотрудникам»? Это Рецепт B, но с несколькими сущностями и статусами. Не изобретай схему — бери эту. + +**Модель данных — один файл `data.json`, внутри списки:** +```js +// { "items": [...], "log": [...] } // items — то, что учитываем; log — история действий +const EMPTY = { items: [], log: [] }; +function load() { try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch { return EMPTY; } } +function save(d) { fs.writeFileSync(DATA, JSON.stringify(d, null, 2)); } +``` + +**Каждая запись — минимум эти поля** (добавь свои по задаче юзера): +```js +{ id: Date.now(), title: "Каска защитная", qty: 12, person: "Иванов И.", + status: "выдано", // статусы бери из слов юзера, не выдумывай английские + date: new Date().toISOString().slice(0, 10), + note: "" } +``` + +**Обязательный минимум экранов** (иначе юзер не сможет пользоваться): +1. **Таблица** всех записей + строка поиска (фильтр по подстроке на клиенте). +2. **Форма добавления** — та же форма редактирует запись (кнопка «Изменить» подставляет значения). +3. **Фильтр по статусу** — кнопки-чипсы над таблицей. +4. **Счётчики сверху**: всего / по каждому статусу. +5. **Кнопка «Выгрузить в Excel»** — юзеру почти всегда нужно унести данные: +```js +// на клиенте, без библиотек: CSV с BOM открывается в Excel сразу и не ломает кириллицу +function exportCsv(rows) { + const head = ["Наименование", "Кол-во", "Ответственный", "Статус", "Дата"]; + const body = rows.map(r => [r.title, r.qty, r.person, r.status, r.date] + .map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";")); + const csv = "\uFEFF" + [head.join(";"), ...body].join("\n"); + const a = document.createElement("a"); + a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); + a.download = "vygruzka.csv"; a.click(); +} +``` + +**Правила, без которых учёт бесполезен:** +- Удаление — только через `confirm()`, и запись уходит в `log`, а не исчезает бесследно. +- Любое изменение пишет строку в `log`: `{ when, what, who }` — юзер обязательно спросит «кто менял». +- Даты показывай как `ДД.ММ.ГГГГ`, хранить можно как есть. +- Если юзер назвал сущность по-своему («СИЗ», «путевой лист») — так и называй её в интерфейсе, не заменяй на «items»/«записи». + +--- + +## 📂 ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ (Excel / Word / PDF) — библиотеки УЖЕ установлены + +В контейнере проекта ПРЕДУСТАНОВЛЕНЫ (НЕ добавляй их в dependencies и НЕ ставь — просто require): +- **Node:** `grammy` (бот), `xlsx` (Excel/CSV), `mammoth` (Word → текст/HTML), `pdf-parse` (PDF → текст), `formidable` (загрузка файлов в приложении). +- **Python:** `openpyxl`, `python-docx`, `pypdf`, `requests`. + +Всё, чего в списке нет, — добавляй в `dependencies`, поставит `run`. + +**Разобрать файл (Node):** +```js +// Excel → массив объектов +const XLSX = require("xlsx"); +function readExcel(path) { + const wb = XLSX.readFile(path); + return XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); +} +// Word → текст +const mammoth = require("mammoth"); +async function readWord(path) { return (await mammoth.extractRawText({ path })).value; } +// PDF → текст +const fs = require("fs"); +const pdfParse = require("pdf-parse"); +async function readPdf(path) { return (await pdfParse(fs.readFileSync(path))).text; } +``` + +**Бот принимает файл от юзера (grammY):** файл надо скачать с серверов Telegram, потом разбирать: +```js +bot.on("message:document", async (ctx) => { + const file = await ctx.getFile(); + const url = "https://api.telegram.org/file/bot" + process.env.BOT_TOKEN + "/" + file.file_path; + const buf = Buffer.from(await (await fetch(url)).arrayBuffer()); + const name = ctx.message.document.file_name || "file"; + if (name.endsWith(".xlsx") || name.endsWith(".xls")) { + const wb = require("xlsx").read(buf); + const rows = require("xlsx").utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); + return ctx.reply("Строк в таблице: " + rows.length); + } + if (name.endsWith(".docx")) { + const t = (await require("mammoth").extractRawText({ buffer: buf })).value; + return ctx.reply("Текст из Word (начало): " + t.slice(0, 300)); + } + if (name.endsWith(".pdf")) { + const t = (await require("pdf-parse")(buf)).text; + return ctx.reply("Текст из PDF (начало): " + t.slice(0, 300)); + } + ctx.reply("Пока умею: .xlsx, .docx, .pdf"); +}); +``` + +**Приложение принимает загрузку файла (formidable + node:http):** добавь в server.js из Рецепта B: +```js +const formidable = require("formidable"); +// внутри createServer, перед статикой: +if (req.method === "POST" && req.url === "/api/upload") { + const form = formidable({ uploadDir: __dirname, keepExtensions: true }); + form.parse(req, (err, fields, files) => { + const f = files.file && files.file[0]; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: !err && !!f, path: f ? f.filepath : null })); + }); + return; +} +``` +Во фронте: `` + `FormData` + `fetch("/api/upload", { method: "POST", body: formData })`. Дальше разбирай файл рецептами выше и отвечай юзеру данными. + +--- + +## 🚑 НЕ ЗАПУСКАЕТСЯ? ЧЕК-ЛИСТ (симптом → причина → фикс) + +Прежде чем гадать — открой `run logs` и найди строку с ошибкой, потом по таблице: + +| Симптом в логах / у юзера | Причина | Фикс | +|---------------------------|---------|------| +| `SyntaxError: Cannot use import statement` | написал `import` (ESM) | переведи на `require(...)` (CommonJS). У нас по умолчанию CommonJS. | +| `run` пишет «не нашёл, что запускать» | нет `package.json` со `scripts.start` и нет `bot.js`/`server.js` | добавь `package.json` с `"scripts": {"start": "node bot.js"}` (или `server.js`) | +| `Error: Cannot find module 'X'` | зависимости `X` нет в `package.json` | добавь `X` в `dependencies`, запусти `run` заново (он поставит). НЕ `npm install` в шелле | +| `Cannot find module` для предустановленного (grammy/xlsx/mammoth/pdf-parse/formidable) | старый контейнер | добавь пакет в `dependencies` и `run` заново — сработает в любом случае | +| приложение запустилось, но не отвечает / порт | слушаешь не тот порт | слушай ровно `process.env.PORT || 3000` | +| `Empty token!` / бот не видит токен | `.env` не загружается | в `package.json` → `"start": "node --env-file=.env bot.js"`; `.env` рядом с `bot.js` | +| бот не отвечает в Telegram | используешь webhook | только `bot.start()` (long-polling), НЕ webhooks; токен в `.env` → `process.env.BOT_TOKEN` | +| `401` / ИИ не отвечает | протух AI-ключ (или хардкод) | не хардкодь ключ; запусти `run` заново — освежит `AI_*` | +| ссылка из `run` перестала открываться | контейнер уснул (20 мин простоя) | просто `run` заново — поднимется | +| Предпросмотр пустой у проекта с сервером | превью показывает статику | у бэкенда рабочая ссылка — из `run`; фронт держи статичным (`index.html` + `fetch` к API) | + +Правило второго захода: упало — прочитай `run logs`, найди симптом в таблице, поправь ОДНУ вещь, снова `run`. Не переписывай всё с нуля. + +--- + +## 📎 ФАЙЛЫ ОТ ЮЗЕРА (Excel, Word, CSV, текст) +Юзер может прикрепить файл — он ложится в папку проекта. **Читать Excel/Word напрямую (`cat`) нельзя** — они бинарные (zip+xml). Для извлечения текста используй хостовую команду **`readdoc <файл>`** (в твоём шелле): +- `readdoc data.xlsx` — все листы Excel как таблицы (значения через табуляцию); +- `readdoc doc.docx` — текст Word по абзацам; +- `readdoc doc.pdf` — текст из PDF (сканы без текстового слоя не прочтёт); +- `readdoc table.csv` / `.tsv` / `.txt` — как есть (можно и `cat`); +- большой файл — ограничь: `readdoc big.xlsx --max 100`. +Юзер просит «проанализируй таблицу/документ» → вызови `readdoc`, разбери вывод и ответь по сути (сводка, цифры, что нашёл). Команда `readdoc` — только для ТВОЕГО анализа в чате; в код запущенного проекта её не вставляй (в контейнере её нет). + +**Не нашёл файл в текущей папке?** Загруженный файл мог лечь в КОРЕНЬ воркспейса (на уровень выше cwd). Проверь `ls ..` и читай/копируй оттуда: `readdoc "../имя файла.xlsx"` или `cp "../имя.xlsx" .`. Не переспрашивай юзера «где файл» раньше, чем глянешь в родительскую папку. + +**Используй ВСЕ данные, не «первые N».** Если строишь из файла таблицу/список/каталог — выведи ВСЕ записи, а не 5-6. **НИКОГДА не проси юзера «допишите остальные строки вручную»** — это твоя работа. Данных много (десятки+ строк)? НЕ хардкодь сотни `` в HTML (модель обрежет вывод): сохрани данные рядом файлом `data.json` (массив объектов) и отрисуй в `index.html` через JS — `fetch('data.json').then(r=>r.json()).then(rows => …)` + `map`/`innerHTML`. Так отрисуются все строки, и появится место для поиска/фильтра. `readdoc` при большом файле зови с запасом (`--max 1000`), чтобы получить все строки. + +--- + +## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет) + +1. Поздоровайся коротко: «Привет! Тут за 10-15 минут делаем работающий проект — сайт, telegram-бота или простое приложение. Что хочешь сделать?» +2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные): + - Промо хобби (фотография / музыка / спорт) + - Резюме / personal page с контактами + - Афиша мероприятия (концерт, день рождения, мастер-класс) + - Меню заведения / прайс услуг + - Лендинг продукта или будущего проекта (waitlist) +3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы). +4. Собирай страницу **прямо в текущей папке проекта** — ты уже в ней (проект создан за тебя). Создавай `index.html`/`style.css`/`script.js` тут же. **НЕ запускай `./new-project`** и не делай `cd` в другие папки. Не спрашивай разрешения на каждый шаг. + +--- + +## 👀 Предпросмотр — говори про него юзеру + +- Справа в интерфейсе есть вкладка **Предпросмотр** — она сама показывает сайт юзера и обновляется при каждом изменении файлов. Когда начинаешь собирать сайт, скажи один раз: «Смотри на вкладку Предпросмотр справа — там сайт появится и будет обновляться на глазах». +- Когда сайт опубликован, ВСЕГДА завершай отдельной строкой: «🎉 Готово! Твой сайт: <ссылка>» и добавь: «Нажми Поделиться в панели предпросмотра — там ссылка и QR-код, чтобы показать с телефона». + +--- + +## 🧠 Память о юзере — веди её сам + +Файл памяти: `/srv/opencode/workspaces/users/genadis/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`. + +**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/genadis/.mimo/memory.md` (если есть) ПЕРЕД первым ответом. Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию. + +**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь: +- как обращаться к юзеру (имя/ник); +- язык общения (русский / казахский / английский); +- уровень (новичок / уверенный); +- интересы и тематику; +- проекты: `repo → тема, статус`; +- стиль-предпочтения (тёмный / яркий, минимализм, ...); +- незакрытые хвосты («хотел добавить фото», «вернёмся к форме»). + +**Формат:** короткий markdown-список, максимум ~30 строк. Старое неактуальное — удаляй, не копи. + +**ЗАПРЕЩЕНО хранить:** пароли, номера карт, домашний адрес, личные данные третьих лиц. + +**Лотус ≠ владелец аккаунта.** Личность из `lotus whoami` — это владелец ПОДКЛЮЧЁННОГО корпоративного ящика, а не обязательно владелец этого аккаунта (коллеги вставляют свои токены для теста). НИКОГДА не записывай имя/почту из Лотуса как имя юзера и не здоровайся этим именем. Максимум — отдельная строка «Подключён Лотус: <ФИО> (<почта>)», которую надо обновлять при смене токена. Обращайся к юзеру только по имени, которым он сам представился в чате. + +**Говори языком юзера.** Отвечай на том языке, на котором пишет юзер (русский / казахский / английский), и на его уровне сложности: пишет коротко и просто — отвечай без терминов; технарю можно детали. Юзер переключил язык — переключись следом. + +**Это касается ВСЕГО видимого текста, а не только финального ответа.** Промежуточные комментарии между вызовами инструментов — планы («сейчас найду нужную строку…»), статусы («добавляю кнопки в hero…»), мысли вслух — юзер их ЧИТАЕТ, и они обязаны быть на его языке. Писать «We'll edit line 139» русскоязычному юзеру — ошибка. По-английски остаются только код, команды и имена файлов. + +--- + +## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО + +Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**. + +Скажи: «Давай определимся, я задам 3 коротких вопроса: +1. Это для тебя лично, для проекта/бизнеса, или для события? +2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио? +3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?» + +После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать. + +--- + +## 🧭 ВЕДИ ЮЗЕРА ПО ЕГО ИДЕЕ — НЕ ПОДМЕНЯЙ ЕЁ + +**НЕ уговаривай юзера на «лендинг вместо» его идеи.** Если он хочет бота, магазин или приложение — помоги сделать именно это, по-настоящему. Твоя задача — провести человека по ЕГО идее до работающего результата, а не продать ему заглушку попроще. + +Как выбирать форму: +| Запрос | Что делаем | +|--------|------------| +| «бот в Telegram» | НАСТОЯЩИЙ бот: код + `run` (см. раздел «Бэкенд-проекты»), long-polling | +| «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` | +| «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp | +| «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` | +| «сайт-визитка / промо / портфолио» | статичный лендинг + публикация в `pages` — тут это лучший инструмент, а не компромисс | +| «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет | + +Правила ведения: +1. Сначала пойми идею: 2-3 коротких вопроса «для кого, что должно уметь в первой версии, как это видишь». +2. Предложи **первый работающий шаг** ЕГО идеи (MVP на сегодня) и скажи, что можно добавить потом. Не ужимай идею молча. +3. Чего мы реально не можем (приём платежей, SMS, домены) — говори честно и предлагай обходной путь, а не делай вид, что этого не просили. +4. **Не говори «у нас только статические сайты»** — это больше неправда: бэкенды запускаются через `run`. + +--- + +## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера) + +### A — Промо продукта/услуги +**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер). + +### B — Personal / резюме +**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки). + +### C — Афиша мероприятия +**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт). + +### D — Меню / прайс +**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка). + +### E — Waitlist для будущего проекта +**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма). + +Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического. + +--- + +## 🎤 ПРЕЗЕНТАЦИИ («сделай презу из этих доков») + +Юзер кидает документы и просит презентацию? Флоу: +1. Прочитай доки командой `readdoc` (умеет .xlsx, .docx, .pdf, .csv/.txt). Файлы могут лежать в корне воркспейса — проверь `ls ..`. +2. Выдели суть: 5-9 слайдов, на слайде заголовок + 3-5 коротких пунктов. Не переноси абзацы целиком — сжимай до тезисов. +3. Возьми скелет ниже (это ОДИН файл `index.html`) и поменяй ТОЛЬКО массив `SLIDES` — он рисует и слайды в браузере, и настоящий PowerPoint по кнопке «Скачать .pptx». Остальной код не трогай. +4. Покажи через Предпросмотр (стрелки/клавиши листают). Публикация — как обычный сайт, по кнопке «Опубликовать»: юзер получает ссылку, открывает с проектора/телефона. +5. Скажи юзеру: «Кнопка Скачать .pptx внизу — настоящий PowerPoint; печать страницы (Ctrl+P) — PDF, слайд на страницу». + +Скелет (файл `index.html`, целиком): +```html + + + + + +Презентация + + + + +
+ +
+ + + +``` + +Правила: цвета/шрифты можно подгонять под тему юзера в ` + + + +
+
+ Название + + О нас + Оставить заявку +
+
+ +
+
+

Главное обещание в одну строку

+

Подзаголовок: кому это и почему стоит остаться.

+ +
+
+ +
+
+
+

Что вы получите

+

Одно-два предложения пояснения к секции.

+
+
+

Пункт первый

Коротко и по делу.

+

Пункт второй

Коротко и по делу.

+

Пункт третий

Коротко и по делу.

+
+
+
+ +
+
+

Призыв к действию

+

Город, адрес, часы работы — то, что нужно, чтобы дойти.

+ Написать +
+
+ +
+ © 2026 Название + + Контакты +
+ + + +``` + +--- + +## Блоки страницы (`kt-ai-page.css`) + +| Класс | Что это | +|-------|---------| +| `.kt-ai-wrap` | контейнер по центру (1080px). `data-width="narrow"` — 720px, `"wide"` — 1200px | +| `.kt-ai-pagehead` + `.kt-ai-pagehead-in` | липкая шапка страницы | +| `.kt-ai-hero`, `.kt-ai-hero-sub`, `.kt-ai-hero-actions` | первый экран. `data-align="center"` — по центру | +| `.kt-ai-section` + `.kt-ai-section-head` (h2 + p) | обычная секция. `data-divider="true"` — линия сверху | +| `.kt-ai-grid` + `.kt-ai-tile` (h3 + p) | сетка плиток «возможности». `data-cols="2"/"4"`, у плитки `data-span="2"/"full"` | +| `.kt-ai-tile-icon` | иконка над заголовком плитки (спрайт — `design-system/icons/`) | +| `.kt-ai-cta` | финальный блок с призывом | +| `.kt-ai-pagefoot` | подвал | +| `.kt-ai-spacer` | распорка внутри шапки/подвала (толкает остальное вправо) | +| `.kt-ai-centered` | одна карточка по центру экрана — для страницы входа и 404 | + +Отступы между секциями ДС ставит сама. Адаптив (3→2→1 колонка) — тоже, `@media` писать не нужно. + +## Элементы (`kt-ai-components.css`) + +| Класс | Как применять | +|-------|---------------| +| `.kt-ai-btn` | кнопка / ссылка-кнопка. `data-variant="primary"` — главная, `"ghost"` — прозрачная, `"danger"` — опасная. `data-size="sm"/"lg"` | +| `.kt-ai-card` | карточка контента | +| `.kt-ai-chip` | статус-пилюля / тег | +| `.kt-ai-link` | текстовая ссылка | +| `.kt-ai-field` + `.kt-ai-input` / `.kt-ai-textarea` / `.kt-ai-select` | поле формы с подписью | + +**Один `primary` на экран.** Остальные кнопки — обычные или `ghost`. + +## Токены (`kt-ai-tokens.css`) + +Цвет: `--kt-ai-bg`, `--kt-ai-bg-soft`, `--kt-ai-fg`, `--kt-ai-fg-muted`, `--kt-ai-fg-faint`, +`--kt-ai-border`, `--kt-ai-divider`, `--kt-ai-card-bg`, `--kt-ai-card-border`, +`--kt-ai-primary`, `--kt-ai-link`, `--kt-ai-danger`. + +Отступы: `--kt-ai-space-1…12` (2px → 64px). Скругления: `--kt-ai-radius-md/lg/xl/2xl/3xl/full`. +Текст: `--kt-ai-text-xs…3xl`. Вес: `--kt-ai-weight-regular/medium/semibold/bold`. +Шрифты: `--kt-ai-font-sans` (Inter), `--kt-ai-font-mono`. + +**Тема** — атрибут на ``: `data-theme="auto"` (по системной, ставь его по умолчанию), +`"light"`, `"dark"`. Проверь страницу в обеих темах — с токенами это бесплатно. + +--- + +## Не лендинг, а приложение (дашборд, таблица, очередь заявок, чат) + +Тогда экран собирается **не вёрсткой руками, а конфигом**: + +1. Возьми `design-system/templates/kt-ai-app-shell.html` — самодостаточный HTML-каркас приложения + (без React и npm). +2. Опиши экран JSON-конфигом по `design-system/docs/PRODUCT_CONTRACT.md` и положи его в тег + `` внутри каркаса — он сам отрисует + таблицу, KPI, фильтры, drawer и темы. +3. Проверь конфиг: `python3 design-system/scripts/validate_product.py config.json --strict` → нужен `0/0`. + +Полные правила для сложных экранов — `design-system/AGENTS.md`, список компонентов — +`design-system/COMPONENTS.md`, приёмка — `design-system/CHECKLIST.md`. Читай их, когда лендинга +уже мало. + +--- + +## 📦 Готовые рецепты + +### Фото без ключей +- Одно и то же слово → одна и та же картинка: `https://picsum.photos/seed/coffee/800/600` +- По теме: `https://loremflickr.com/800/600/coffee` (тег прямо в URL) + +Вставляй с рамкой ДС: `…` + +### Кнопка WhatsApp +```html +Написать в WhatsApp +``` +`7XXXXXXXXXX` — номер в международном формате без `+`. + +### Форма заявки без бэкенда +```html +
+
+
+ +
+``` +Скажи юзеру: замени `EMAIL` на свою почту — заявки будут приходить туда (первая заявка активирует форму). + +### Favicon-эмодзи (без файла) +```html + +``` + +### Карта +На maps.google.com: место → «Поделиться» → «Встроить карту» → вставь iframe как есть: +```html + +``` + +### Плавные появления при скролле +```html + + +``` + +--- + +## Перед тем как сказать «готово» + +- [ ] Подключены все четыре CSS из `design-system/`, папка лежит в проекте (не забудь закоммитить её). +- [ ] В вёрстке нет сырых hex-цветов и чужих шрифтов. +- [ ] Страница нормально выглядит в светлой и тёмной теме и на телефоне (узкое окно). +- [ ] Один `data-variant="primary"` на экран, тексты — про юзера, а не «Lorem ipsum». diff --git a/import-servers.js b/import-servers.js new file mode 100644 index 0000000..8e89c9e --- /dev/null +++ b/import-servers.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const path = require('path'); + +// Чтение файла +const content = fs.readFileSync(path.join(__dirname, 'список серверов.txt'), 'utf8'); +const lines = content.trim().split('\n'); +const header = lines[0].split('\t'); +const rows = lines.slice(1).map(line => { + const cols = line.split('\t'); + return { + admin: cols[0], + city: cols[1], + region: cols[2], + ip: cols[3], + cityCode: cols[4] + }; +}); + +// Чтение текущих данных +const dataPath = path.join(__dirname, 'data.json'); +let data = JSON.parse(fs.readFileSync(dataPath, 'utf8')); + +// Уникальные админы +const adminMap = new Map(); +rows.forEach(row => { + if (!adminMap.has(row.admin)) { + adminMap.set(row.admin, { + name: row.admin, + position: '', + phone: '', + email: '' + }); + } +}); + +// Добавляем новых админов +const existingAdmins = new Set(data.admins.map(a => a.name)); +const newAdmins = []; +adminMap.forEach((admin, name) => { + if (!existingAdmins.has(name)) { + newAdmins.push({ + id: Date.now() + Math.random(), + ...admin + }); + console.log('Добавлен админ:', name); + } +}); +data.admins = [...data.admins, ...newAdmins]; + +// Добавляем серверы (уникальные по IP) +const existingIps = new Set(data.servers.map(s => s.ip)); +const newServers = []; +rows.forEach(row => { + if (!existingIps.has(row.ip)) { + newServers.push({ + id: Date.now() + Math.random(), + admin: row.admin, + city: row.city, + region: row.region, + cityCode: row.cityCode, + ip: row.ip + }); + console.log('Добавлен сервер:', row.city, row.ip); + } +}); +data.servers = [...data.servers, ...newServers]; + +// Сохранение +fs.writeFileSync(dataPath, JSON.stringify(data, null, 2)); +console.log('\nГотово!'); +console.log('Добавлено админов:', newAdmins.length); +console.log('Добавлено серверов:', newServers.length); diff --git a/index.html b/index.html new file mode 100644 index 0000000..d0550e8 --- /dev/null +++ b/index.html @@ -0,0 +1,1973 @@ + + + + + + Инфраструктура - Дашборд + + + + + + +
+

Дашборд

+
+
0
Задач всего
+
0
Высокий приоритет
+
0
Систем
+
0
Серверов
+
0
Админов
+
0
Пользователей
+
+
+

Распределение по админам

+
+
+
+ + +
+

Список дел

+
+ +
+ + +
+

Общие задачи

+

Задачи создаёт администратор. Каждый пользователь отмечает выполнение только у себя.

+ +
+ +
+ + + + + + + + + + + + +
ЗадачаПриоритетСрокАвторСтатус у меняДействия
+
+ + +
+

Личные задачи

+

Ваши персональные задачи, видны только вам.

+ +
+ + +
+ + + + + + + + + + + +
ЗадачаПриоритетСрокСтатусДействия
+
+
+ + +
+

Системы

+ +
+ +
+ + + + + + + + + + + + +
СистемаАдресОписаниеОтветственныйТехподдержкаДействия
+
+ + +
+

Серверы

+
+ + +
+
+ +
+ + + + + + + + + + + +
ГородРегионКод городаIPОписание
+
+ + +
+

Коллеги-админы

+
+ + + +
+
+ +
+ + + + + + + + + + + + + + +
ФИОДолжностьРабочий телДомашний телСотовыйEmailСерверыДействия
+
+ + +
+

Операторы

+ +
+ +
+ + + + + + + + + + + + + + +
ФИОЛогинСистемаОснованиеРежимДоп.ДатаДействия
+
+ + +
+

Распределение систем по админам

+

Данные автоматически берутся из вкладки Коллеги-админы. Добавляйте серверы там.

+
+ +
+ + + + + + + + + + + +
Админ ↕Город ↕Код города ↕IP сервера ↕Действия
+
+ + +
+

Пользователи приложения

+
+ + +
+

Кнопка создаст пользователей приложения для всех админов из вкладки Коллеги-админы с правами оператора по умолчанию.

+
+ +
+ + + + + + + + + + + + + + + + +
ЛогинФИОДолжностьТелефонEmailГородКод городаIP адресРольДействия
+
+ + +
+

Роли

+
+ +
+ + + + + + + + + +
НазваниеОписаниеДействия
+
+ + +
+

Лог аудита

+
+ + +
+ + + + + + + + + + +
ВремяПользовательДействиеДетали
+
+ + +
+

Личный кабинет

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + diff --git a/init-admin.js b/init-admin.js new file mode 100644 index 0000000..f125368 --- /dev/null +++ b/init-admin.js @@ -0,0 +1,21 @@ +const fs = require("fs"); +const path = require("path"); + +const DATA = path.join(__dirname, "data.json"); + +let data; +try { + data = JSON.parse(fs.readFileSync(DATA, "utf8")); +} catch (_) { + data = { todos: [], systems: [], servers: [], admins: [], users: [], distribution: [], appUsers: [], auditLog: [] }; +} + +if (!data.appUsers || data.appUsers.length === 0) { + data.appUsers = [ + { login: "admin", password: "admin123", name: "Администратор", role: "admin" } + ]; + fs.writeFileSync(DATA, JSON.stringify(data, null, 2)); + console.log("Создан пользователь admin / admin123 (роль: admin)"); +} else { + console.log("Пользователи уже существуют"); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0660183 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "infra-dashboard", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "infra-dashboard" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1e0877d --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "infra-dashboard", + "private": true, + "type": "commonjs", + "scripts": { + "start": "node init-admin.js && node server.js" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..aa7b1c0 --- /dev/null +++ b/server.js @@ -0,0 +1,828 @@ +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const PORT = process.env.PORT || 3000; +const DATA = path.join(__dirname, "data.json"); + +const EMPTY = { + commonTodos: [], + personalTodos: [], + systems: [], + servers: [], + admins: [], + users: [], + distribution: [], + appUsers: [], + auditLog: [] +}; + +function logAction(user, action, details) { + const data = load(); + data.auditLog.push({ + id: Date.now(), + when: new Date().toISOString(), + user: user, + action: action, + details: details + }); + if (data.auditLog.length > 1000) data.auditLog = data.auditLog.slice(-1000); + save(data); +} + +function load() { + try { + return JSON.parse(fs.readFileSync(DATA, "utf8")); + } catch (_) { + return EMPTY; + } +} + +function save(data) { + fs.writeFileSync(DATA, JSON.stringify(data, 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, data) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(data)); +} + +function notFound(res) { + res.writeHead(404); + res.end("Not found"); +} + +function forbidden(res) { + res.writeHead(403); + res.end(JSON.stringify({ error: "Forbidden" })); +} + +function checkPermission(req, section, action) { + const user = req.headers["x-user"]; + if (!user) return false; + const data = load(); + const appUser = (data.appUsers || []).find(u => u.login === user); + if (!appUser) return false; + + const role = appUser.role; + + // Admin — полный доступ + if (role === "admin") return true; + + // Оператор — только просмотр на отдельные вкладки + создание личных задач + if (role === "operator") { + if (action === "view") { + // Оператор видит: дашборд, системы, серверы, админы, операторы, распределение + const viewSections = ["dashboard", "systems", "servers", "admins", "operators", "distribution"]; + return viewSections.includes(section); + } + // Оператор может создавать только личные задачи + if (action === "create" && section === "personal-todos") return true; + if (action === "complete" && section === "todos") return true; // закрывать свои задачи + return false; + } + + // Модератор — все вкладки кроме "Пользователи приложения" + гибкие права + if (role === "moderator") { + const perms = appUser.permissions || {}; + // Проверка доступа к вкладке + if (action === "view") { + // Модератор НЕ видит "Пользователи приложения" + if (section === "appUsers" || section === "users") return false; + const sectionPerms = perms[section] || {}; + return sectionPerms.view === true; + } + // Проверка действий (create, edit, delete) + if (section === "appUsers" || section === "users") return false; + const sectionPerms = perms[section] || {}; + return sectionPerms[action] === true; + } + + return false; +} + +http.createServer(async (req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-User"); + + if (req.method === "OPTIONS") { + res.writeHead(200, { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, X-User" + }); + res.end(); + return; + } + + const url = req.url.split("?")[0]; + console.log("Request:", req.method, url); + + // API: Todos + // API: Common Todos (общие задачи) + if (req.method === "GET" && url === "/api/common-todos") { + const data = load(); + return json(res, data.commonTodos || []); + } + if (req.method === "POST" && url === "/api/common-todos") { + const data = load(); + const user = req.headers["x-user"]; + const item = await body(req); + item.id = Date.now(); + item.createdBy = user; + item.createdAt = new Date().toISOString(); + // userStates: { login: { completed: boolean, completedAt: string|null } } + item.userStates = {}; + data.commonTodos.push(item); + save(data); + logAction(user, "create_common_todo", item.title); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/common-todos/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + const idx = data.commonTodos.findIndex((t) => t.id === id); + if (idx !== -1) { + const bodyData = await body(req); + // Если есть userState, обновляем только для текущего пользователя + if (bodyData.userState !== undefined) { + if (!data.commonTodos[idx].userStates) data.commonTodos[idx].userStates = {}; + data.commonTodos[idx].userStates[user] = bodyData.userState; + } else { + data.commonTodos[idx] = { ...data.commonTodos[idx], ...bodyData }; + } + save(data); + logAction(user, "update_common_todo", "id=" + id); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/common-todos/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + data.commonTodos = data.commonTodos.filter((t) => t.id !== id); + save(data); + logAction(user, "delete_common_todo", "id=" + id); + return json(res, { ok: true }); + } + + // API: Personal Todos (личные задачи) + if (req.method === "GET" && url === "/api/personal-todos") { + const user = req.headers["x-user"]; + const data = load(); + const todos = data.personalTodos.filter(t => t.createdBy === user); + return json(res, todos); + } + if (req.method === "POST" && url === "/api/personal-todos") { + const data = load(); + const user = req.headers["x-user"]; + const item = await body(req); + item.id = Date.now(); + item.completed = false; + item.completedAt = null; + item.createdBy = user; + data.personalTodos.push(item); + save(data); + logAction(user, "create_personal_todo", item.title); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/personal-todos/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + const idx = data.personalTodos.findIndex((t) => t.id === id); + if (idx !== -1) { + data.personalTodos[idx] = { ...data.personalTodos[idx], ...(await body(req)) }; + save(data); + logAction(user, "update_personal_todo", "id=" + id); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/personal-todos/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + data.personalTodos = data.personalTodos.filter((t) => t.id !== id); + save(data); + logAction(user, "delete_personal_todo", "id=" + id); + return json(res, { ok: true }); + } + + // API: Systems + if (req.method === "GET" && url === "/api/systems") { + return json(res, load().systems); + } + if (req.method === "POST" && url === "/api/systems") { + if (!checkPermission(req, "systems", "edit")) return forbidden(res); + const data = load(); + const item = await body(req); + item.id = Date.now(); + data.systems.push(item); + save(data); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/systems/")) { + if (!checkPermission(req, "systems", "edit")) return forbidden(res); + const id = parseInt(url.split("/").pop()); + const data = load(); + const idx = data.systems.findIndex((s) => s.id === id); + if (idx !== -1) { + data.systems[idx] = { ...data.systems[idx], ...(await body(req)) }; + save(data); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/systems/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + data.systems = data.systems.filter((s) => s.id !== id); + save(data); + return json(res, { ok: true }); + } + + // API: Servers + if (req.method === "GET" && url === "/api/servers") { + return json(res, load().servers); + } + if (req.method === "POST" && url === "/api/servers") { + if (!checkPermission(req, "servers", "edit")) return forbidden(res); + const data = load(); + const item = await body(req); + item.id = Date.now(); + data.servers.push(item); + save(data); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/servers/")) { + if (!checkPermission(req, "servers", "edit")) return forbidden(res); + const id = parseInt(url.split("/").pop()); + const data = load(); + const idx = data.servers.findIndex((s) => s.id === id); + if (idx !== -1) { + data.servers[idx] = { ...data.servers[idx], ...(await body(req)) }; + save(data); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/servers/")) { + if (!checkPermission(req, "servers", "edit")) return forbidden(res); + const id = parseInt(url.split("/").pop()); + const data = load(); + data.servers = data.servers.filter((s) => s.id !== id); + save(data); + return json(res, { ok: true }); + } + + // API: Admins + if (req.method === "GET" && url === "/api/admins") { + return json(res, load().admins); + } + if (req.method === "POST" && url === "/api/admins") { + const data = load(); + const item = await body(req); + item.id = Date.now(); + data.admins.push(item); + save(data); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/admins/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const idx = data.admins.findIndex((a) => a.id === id); + if (idx !== -1) { + data.admins[idx] = { ...data.admins[idx], ...(await body(req)) }; + save(data); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/admins/")) { + const idParam = url.split("/").pop(); + const id = idParam.includes('.') ? parseFloat(idParam) : parseInt(idParam); + const data = load(); + data.admins = data.admins.filter((a) => a.id !== id); + save(data); + return json(res, { ok: true }); + } + + // API: Admin Servers (multiple servers per admin) + if (req.method === "GET" && url === "/api/admin-servers") { + return json(res, load().adminServers || []); + } + if (req.method === "POST" && url === "/api/admin-servers") { + const data = load(); + const item = await body(req); + item.id = Date.now(); + if (!data.adminServers) data.adminServers = []; + data.adminServers.push(item); + save(data); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/admin-servers/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const idx = (data.adminServers || []).findIndex((s) => s.id === id); + if (idx !== -1) { + data.adminServers[idx] = { ...data.adminServers[idx], ...(await body(req)) }; + save(data); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/admin-servers/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + data.adminServers = (data.adminServers || []).filter((s) => s.id !== id); + save(data); + return json(res, { ok: true }); + } + + // API: Import servers from file + if (req.method === "POST" && url === "/api/import-servers") { + const data = load(); + const fs = require("fs"); + const path = require("path"); + const filePath = path.join(__dirname, "список серверов.txt"); + if (!fs.existsSync(filePath)) { + return json(res, { ok: false, error: "Файл не найден" }); + } + const content = fs.readFileSync(filePath, "utf8"); + const lines = content.trim().split("\n").slice(1); + const rows = lines.map(line => { + const cols = line.split("\t"); + return { admin: cols[0], city: cols[1], region: cols[2], ip: cols[3], cityCode: cols[4] }; + }); + + const existingAdmins = new Set(data.admins.map(a => a.name)); + rows.forEach(row => { + if (!existingAdmins.has(row.admin)) { + data.admins.push({ id: Date.now() + Math.random(), name: row.admin, position: "", phone: "", email: "" }); + existingAdmins.add(row.admin); + } + }); + + const existingIps = new Set(data.servers.map(s => s.ip)); + const addedIps = new Set(); + rows.forEach(row => { + const ip = row.ip.trim(); + if (!existingIps.has(ip) && !addedIps.has(ip)) { + data.servers.push({ id: Date.now() + Math.random(), admin: row.admin, city: row.city, region: row.region, cityCode: row.cityCode.trim(), ip: ip }); + addedIps.add(ip); + } + }); + + save(data); + return json(res, { ok: true, count: rows.length }); + } + + // API: Import admins from file (старый формат) + if (req.method === "POST" && url === "/api/import-admins") { + const data = load(); + const fs = require("fs"); + const path = require("path"); + const filePath = path.join(__dirname, "список админов.txt"); + if (!fs.existsSync(filePath)) { + return json(res, { ok: false, error: "Файл не найден" }); + } + const content = fs.readFileSync(filePath, "utf8"); + const lines = content.trim().split("\n").slice(1); + const rows = lines.map(line => { + const cols = line.split("\t"); + return { + name: cols[0], + workPhone: cols[1], + homePhone: cols[2], + mobilePhone: cols[3], + city: cols[4] + }; + }); + + const existingNames = new Set(data.admins.map(a => a.name)); + let added = 0; + rows.forEach(row => { + if (!existingNames.has(row.name)) { + data.admins.push({ + id: Date.now() + Math.random(), + name: row.name, + position: "", + workPhone: row.workPhone || "", + homePhone: row.homePhone || "", + mobilePhone: row.mobilePhone || "", + email: "" + }); + existingNames.add(row.name); + added++; + } + }); + + save(data); + return json(res, { ok: true, count: added }); + } + + // API: Import admins from file -1 (обновление существующих) + if (req.method === "POST" && url === "/api/import-admins-1") { + const data = load(); + const fs = require("fs"); + const path = require("path"); + const filePath = path.join(__dirname, "список админов-1.txt"); + if (!fs.existsSync(filePath)) { + return json(res, { ok: false, error: "Файл не найден" }); + } + const content = fs.readFileSync(filePath, "utf8"); + const lines = content.trim().split("\n").slice(1); + const rows = lines.map(line => { + const cols = line.split("\t"); + return { + name: cols[0], + workPhone: cols[1], + homePhone: cols[2], + mobilePhone: cols[3], + city: cols[4] + }; + }); + + let updated = 0; + let added = 0; + rows.forEach(row => { + const idx = data.admins.findIndex(a => a.name === row.name); + if (idx !== -1) { + // Обновляем существующего + data.admins[idx].workPhone = row.workPhone; + data.admins[idx].homePhone = row.homePhone; + data.admins[idx].mobilePhone = row.mobilePhone; + updated++; + } else { + // Добавляем нового + data.admins.push({ + id: Date.now() + Math.random(), + name: row.name, + position: "", + workPhone: row.workPhone || "", + homePhone: row.homePhone || "", + mobilePhone: row.mobilePhone || "", + email: "" + }); + added++; + } + }); + + save(data); + return json(res, { ok: true, updated, added }); + } + + // API: Users + if (req.method === "GET" && url === "/api/users") { + return json(res, load().users); + } + if (req.method === "POST" && url === "/api/users") { + const data = load(); + const item = await body(req); + item.id = Date.now(); + data.users.push(item); + save(data); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/users/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const idx = data.users.findIndex((u) => u.id === id); + if (idx !== -1) { + data.users[idx] = { ...data.users[idx], ...(await body(req)) }; + save(data); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/users/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + data.users = data.users.filter((u) => u.id !== id); + save(data); + return json(res, { ok: true }); + } + + // API: Distribution + if (req.method === "GET" && url === "/api/distribution") { + return json(res, load().distribution); + } + if (req.method === "POST" && url === "/api/distribution") { + const data = load(); + const user = req.headers["x-user"]; + const item = await body(req); + item.id = Date.now(); + data.distribution.push(item); + save(data); + logAction(user, "create_distribution", JSON.stringify(item)); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/distribution/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + const idx = data.distribution.findIndex((d) => d.id === id); + if (idx !== -1) { + data.distribution[idx] = { ...data.distribution[idx], ...(await body(req)) }; + save(data); + logAction(user, "update_distribution", "id=" + id); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/distribution/")) { + const id = parseInt(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + data.distribution = data.distribution.filter((d) => d.id !== id); + save(data); + logAction(user, "delete_distribution", "id=" + id); + return json(res, { ok: true }); + } + + // API: Auth + if (req.method === "POST" && url === "/api/login") { + const data = load(); + const { login, password } = await body(req); + const user = data.appUsers.find((u) => u.login === login && u.password === password); + if (user) { + logAction(login, "login", "ok"); + const permissions = user.role === 'admin' ? 'all' : (user.permissions || {}); + return json(res, { ok: true, user: { login: user.login, role: user.role, name: user.name, permissions } }); + } + logAction(login || "unknown", "login", "failed"); + res.writeHead(401, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: false, error: "Неверный логин или пароль" })); + } + + // API: Profile текущего пользователя + if (req.method === "GET" && url === "/api/profile") { + const user = req.headers["x-user"]; + if (!user) return forbidden(res); + const data = load(); + const appUser = data.appUsers.find((u) => u.login === user); + if (!appUser) return forbidden(res); + // Поддержка старых полей для совместимости + return json(res, { + login: appUser.login, + name: appUser.name, + position: appUser.position, + workPhone: appUser.workPhone || appUser.phone || "", + homePhone: appUser.homePhone || "", + mobilePhone: appUser.mobilePhone || "", + email: appUser.email || "", + city: appUser.city || "", + ipAddress: appUser.ipAddress || appUser.dbAddress || "", + password: appUser.password || "" + }); + } + if (req.method === "PUT" && url === "/api/profile") { + const user = req.headers["x-user"]; + if (!user) return forbidden(res); + const data = load(); + const idx = data.appUsers.findIndex((u) => u.login === user); + if (idx === -1) return forbidden(res); + const upd = await body(req); + // Сохраняем новые поля, оставляем старые для совместимости + data.appUsers[idx] = { + ...data.appUsers[idx], + ...upd, + workPhone: upd.workPhone !== undefined ? upd.workPhone : data.appUsers[idx].workPhone, + homePhone: upd.homePhone !== undefined ? upd.homePhone : data.appUsers[idx].homePhone, + mobilePhone: upd.mobilePhone !== undefined ? upd.mobilePhone : data.appUsers[idx].mobilePhone, + ipAddress: upd.ipAddress !== undefined ? upd.ipAddress : data.appUsers[idx].ipAddress + }; + save(data); + logAction(user, "update_profile", user); + return json(res, { ok: true }); + } + + // API: Users (app users management) + if (req.method === "GET" && url === "/api/app-users") { + const users = load().appUsers.map((u) => ({ login: u.login, role: u.role, name: u.name, position: u.position, phone: u.phone, workPhone: u.workPhone, homePhone: u.homePhone, mobilePhone: u.mobilePhone, email: u.email, city: u.city, cityCode: u.cityCode, dbAddress: u.dbAddress, ipAddress: u.ipAddress, permissions: u.permissions })); + return json(res, users); + } + if (req.method === "POST" && url === "/api/app-users") { + const data = load(); + const user = req.headers["x-user"]; + const item = await body(req); + if (data.appUsers.find((u) => u.login === item.login)) { + res.writeHead(400, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: false, error: "Пользователь существует" })); + } + data.appUsers.push({ + login: item.login, + password: item.password, + name: item.name || "", + position: item.position || "", + email: item.email || "", + city: item.city || "", + workPhone: item.workPhone || item.phone || "", + homePhone: item.homePhone || "", + mobilePhone: item.mobilePhone || "", + ipAddress: item.ipAddress || item.dbAddress || "", + role: item.role, + permissions: item.role === "moderator" ? item.permissions || {} : {} + }); + save(data); + logAction(user, "create_user", item.login); + return json(res, { ok: true }); + } + if (req.method === "PUT" && url.startsWith("/api/app-users/")) { + const login = decodeURIComponent(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + const idx = data.appUsers.findIndex((u) => u.login === login); + if (idx !== -1) { + const upd = await body(req); + // Сохраняем новые поля и поддерживаем старые для совместимости + data.appUsers[idx] = { + ...data.appUsers[idx], + ...upd, + workPhone: upd.workPhone !== undefined ? upd.workPhone : data.appUsers[idx].workPhone, + homePhone: upd.homePhone !== undefined ? upd.homePhone : data.appUsers[idx].homePhone, + mobilePhone: upd.mobilePhone !== undefined ? upd.mobilePhone : data.appUsers[idx].mobilePhone, + ipAddress: upd.ipAddress !== undefined ? upd.ipAddress : data.appUsers[idx].ipAddress + }; + save(data); + logAction(user, "update_user", login); + } + return json(res, { ok: true }); + } + if (req.method === "DELETE" && url.startsWith("/api/app-users/")) { + const login = decodeURIComponent(url.split("/").pop()); + const data = load(); + const user = req.headers["x-user"]; + data.appUsers = data.appUsers.filter((u) => u.login !== login); + save(data); + logAction(user, "delete_user", login); + return json(res, { ok: true }); + } + + // API: Sync admins to app-users + if (req.method === "POST" && url === "/api/sync-admins-to-users") { + const data = load(); + const user = req.headers["x-user"]; + let created = 0; + let updated = 0; + + (data.admins || []).forEach(admin => { + // Используем login из админа, если нет - генерируем из имени + let login = admin.login || ''; + if (!login) { + login = (admin.name || '').toLowerCase().replace(/[^a-zа-яё0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, ''); + } + if (!login) return; + + const existing = data.appUsers.find(u => u.login === login); + if (!existing) { + // Создаём нового пользователя с правами оператора по умолчанию + data.appUsers.push({ + login: login, + password: "password123", + name: admin.name || "", + position: admin.position || "", + phone: admin.workPhone || admin.phone || "", + email: admin.email || "", + city: admin.city || "", + cityCode: admin.cityCode || "", + dbAddress: "", + role: "operator", + permissions: {} + }); + created++; + logAction(user, "create_user_from_admin", login); + } else { + // Обновляем данные существующего + existing.name = admin.name || existing.name; + existing.position = admin.position || existing.position; + existing.phone = admin.workPhone || admin.phone || existing.phone; + existing.email = admin.email || existing.email; + existing.city = admin.city || existing.city; + existing.cityCode = admin.cityCode || existing.cityCode; + updated++; + } + }); + + save(data); + logAction(user, "sync_admins", "created=" + created + ", updated=" + updated); + return json(res, { ok: true, created, updated }); + } + + // API: Audit log + if (req.method === "GET" && url === "/api/audit") { + return json(res, load().auditLog.reverse().slice(0, 200)); + } + + // API: Roles (admin only) + if (req.method === "GET" && url === "/api/roles") { + const user = req.headers["x-user"]; + const data = load(); + const appUser = (data.appUsers || []).find(u => u.login === user); + if (!appUser || appUser.role !== "admin") return forbidden(res); + return json(res, data.roles || []); + } + + if (req.method === "POST" && url === "/api/roles") { + const user = req.headers["x-user"]; + const data = load(); + const appUser = (data.appUsers || []).find(u => u.login === user); + if (!appUser || appUser.role !== "admin") return forbidden(res); + const b = await body(req); + if (!data.roles) data.roles = []; + if (data.roles.find(r => r.name === b.name)) return json(res, { error: "Роль существует" }, 400); + data.roles.push({ name: b.name, description: b.description || "", permissions: b.permissions || {} }); + save(data); + logAction(user, "create", "role: " + b.name); + return json(res, { ok: true }); + } + + if (req.method === "PUT" && url.startsWith("/api/roles/")) { + const user = req.headers["x-user"]; + const data = load(); + const appUser = (data.appUsers || []).find(u => u.login === user); + if (!appUser || appUser.role !== "admin") return forbidden(res); + const oldName = decodeURIComponent(url.split("/")[3]); + const role = (data.roles || []).find(r => r.name === oldName); + if (!role) return notFound(res); + const b = await body(req); + if (b.name && b.name !== oldName && data.roles.find(r => r.name === b.name)) { + return json(res, { error: "Роль с таким именем существует" }, 400); + } + role.name = b.name || oldName; + role.description = b.description !== undefined ? b.description : role.description; + role.permissions = b.permissions !== undefined ? b.permissions : role.permissions; + save(data); + logAction(user, "edit", "role: " + oldName); + return json(res, { ok: true }); + } + + if (req.method === "DELETE" && url.startsWith("/api/roles/")) { + const user = req.headers["x-user"]; + const data = load(); + const appUser = (data.appUsers || []).find(u => u.login === user); + if (!appUser || appUser.role !== "admin") return forbidden(res); + const name = decodeURIComponent(url.split("/")[3]); + if (name === "admin") return json(res, { error: "Нельзя удалить роль admin" }, 400); + const idx = (data.roles || []).findIndex(r => r.name === name); + if (idx === -1) return notFound(res); + data.roles.splice(idx, 1); + // Сбросить роль у пользователей + (data.appUsers || []).forEach(u => { if (u.role === name) u.role = "operator"; }); + save(data); + logAction(user, "delete", "role: " + name); + return json(res, { ok: true }); + } + + // API: Update moderator permissions (admin only) + if (req.method === "POST" && url === "/api/update-permissions") { + const user = req.headers["x-user"]; + const data = load(); + const appUser = (data.appUsers || []).find(u => u.login === user); + if (!appUser || appUser.role !== "admin") return forbidden(res); + + (data.appUsers || []).forEach(u => { + if (u.role === "moderator" && u.permissions) { + const old = u.permissions; + u.permissions = {}; + ["todos", "systems", "servers", "admins", "operators", "distribution", "audit"].forEach(section => { + const oldPerm = old[section] || {}; + u.permissions[section] = { + view: oldPerm.view === true, + create: oldPerm.edit === true, + edit: oldPerm.edit === true, + delete: oldPerm.edit === true + }; + }); + } + if (u.role === "operator" && !u.permissions) { + u.permissions = {}; + } + }); + + save(data); + return json(res, { ok: true }); + } + + // Static files + let file = url === "/" ? "/index.html" : url; + const full = path.join(__dirname, file); + if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { + const ext = path.extname(full); + const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; + res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); + return res.end(fs.readFileSync(full)); + } + + notFound(res); +}).listen(PORT, () => console.log("Server running on port " + PORT)); diff --git a/список админов-1.txt b/список админов-1.txt new file mode 100644 index 0000000..aafb4f9 --- /dev/null +++ b/список админов-1.txt @@ -0,0 +1,11 @@ +ФИО Р.тел Дом.тел. Моб.тел. Город +Абиров Бауыржан Мусрепович (7252) 53-71-90 8 707 705 89 33 8 707 705 89 33 Шымкент +Амирханова Марина Викторовна +7272979054 (вн. 2354). 727 3028888 87052084740 Алматы +Жуков Алексей Сергеевич (7152) 49-64-55 8(7152)466906 8 705 456 82 71 8 715 249 64 55 Петропавловск +Кожина Инна Анатольевна (7212) 43-39-33 (7212) 43-42-26 (700)337-15-20 Караганда +Кравецкий Виталий Николаевич (7102) 76-10-89 (7102) 73-79-60 (705) 161-51-58 Жезказган +Махсутова Захидам Султановна +7272979053 (вн. 2353). 727 386 81 08 8 777 389 33 18 Алматы +Назипов Рамис Дамирович (7252) 53-72-12 8 (7252) 53 11 29 8 701-275-33-26 Шымкент +Оралбаев Кайсар Нагашбекулы 7272979055 (вн. 2355). (727) 309-97-76 (701) 817-84-82 Алматы +Тен Геннадий Вячеславович (7262) 43-94-12 (7262) 46-63-82 (707) 273-95-75 Тараз +Шорабек Дастан Усенулы (7252) 53-00-88 - 8 747 521 10 41 Шымкент \ No newline at end of file diff --git a/список админов.txt b/список админов.txt new file mode 100644 index 0000000..aafb4f9 --- /dev/null +++ b/список админов.txt @@ -0,0 +1,11 @@ +ФИО Р.тел Дом.тел. Моб.тел. Город +Абиров Бауыржан Мусрепович (7252) 53-71-90 8 707 705 89 33 8 707 705 89 33 Шымкент +Амирханова Марина Викторовна +7272979054 (вн. 2354). 727 3028888 87052084740 Алматы +Жуков Алексей Сергеевич (7152) 49-64-55 8(7152)466906 8 705 456 82 71 8 715 249 64 55 Петропавловск +Кожина Инна Анатольевна (7212) 43-39-33 (7212) 43-42-26 (700)337-15-20 Караганда +Кравецкий Виталий Николаевич (7102) 76-10-89 (7102) 73-79-60 (705) 161-51-58 Жезказган +Махсутова Захидам Султановна +7272979053 (вн. 2353). 727 386 81 08 8 777 389 33 18 Алматы +Назипов Рамис Дамирович (7252) 53-72-12 8 (7252) 53 11 29 8 701-275-33-26 Шымкент +Оралбаев Кайсар Нагашбекулы 7272979055 (вн. 2355). (727) 309-97-76 (701) 817-84-82 Алматы +Тен Геннадий Вячеславович (7262) 43-94-12 (7262) 46-63-82 (707) 273-95-75 Тараз +Шорабек Дастан Усенулы (7252) 53-00-88 - 8 747 521 10 41 Шымкент \ No newline at end of file diff --git a/список серверов.txt b/список серверов.txt new file mode 100644 index 0000000..8bbaf23 --- /dev/null +++ b/список серверов.txt @@ -0,0 +1,32 @@ +Администратор Город Регион Адрес БД Код города +Амирханова Марина Викторовна Зыряновск Восточно-Казахстанский ДЭСД г.Алтай 10.189.217.8 72335 +Амирханова Марина Викторовна Зыряновск Восточно-Казахстанский ДЭСД г.Алтай 10.189.217.8 72335 +Амирханова Марина Викторовна Риддер Восточно-Казахстанский ДЭСД 10.189.217.9 72336 +Амирханова Марина Викторовна Семей Восточно-Казахстанский ДЭСД 10.189.217.6 7222 +Амирханова Марина Викторовна Степногорск Акмолинскии ДЭСД 10.71.132.202 71645 +Амирханова Марина Викторовна Усть Каменогорск Восточно-Казахстанский ДСЭД 10.189.217.7 7232 +Амирханова Марина Викторовна Щучинск Акмолинскии ДЭСД 10.71.132.203 71636 +Кожина Инна Анатольевна Балхаш Карагандинский ДЭСД 10.189.217.14 71036 +Кожина Инна Анатольевна Жезказган Карагандинский ДЭСД 10.189.217.15 7102 +Кожина Инна Анатольевна Караганда Карагандинский ДЭСД 10.189.217.10 7212 +Кожина Инна Анатольевна Сатпаев Карагандинский ДЭСД 10.189.217.11 71063 +Кожина Инна Анатольевна Темиртау Карагандинский ДЭСД 10.189.217.12 7213 +Кожина Инна Анатольевна Шахтинск Карагандинский ДЭСД 10.189.217.13 72156 +Кравецкий Виталий Николаевич Аксу Павлодарская ДЭСД 10.189.217.37 71837 +Кравецкий Виталий Николаевич Актобе Актюбинский ДЭСД 10.71.132.197 7132 +Кравецкий Виталий Николаевич Астана Акмолинскии ДЭСД 10.71.132.196 7172 +Кравецкий Виталий Николаевич Кокшетау Акмолинскии ДЭСД 10.71.132.201 7162 +Кравецкий Виталий Николаевич Павлодар Павлодарская ДЭСД 10.189.217.36 7182 +Кравецкий Виталий Николаевич Петропавловск Северо Казахстанский Департаменты эксплуатации сети доступа (ДЭСД) 10.189.217.5 7152 +Кравецкий Виталий Николаевич Экибастуз Павлодарская ДЭСД 10.189.217.38 7187 +Махсутова Захидам Султановна Алматы Алматинский ДЭСД 10.8.29.68 727 +Махсутова Захидам Султановна Талдыкорган Алматинский ДЭСД 10.8.29.69 7282 +Тен Геннадий Вячеславович Актау Мангистауский ДЭСД 10.71.132.199 7292 +Тен Геннадий Вячеславович Атырау Атырауский ДЭСД 10.71.132.198 7122 +Тен Геннадий Вячеславович Тараз Жамбылский ДСЭД 10.8.29.71 7262 +Тен Геннадий Вячеславович Уральск Западно-Казахстанский ДЭСД 10.71.132.200 7112 +Шорабек Дастан Усенулы Костанай Костанойский Департамент эксплуатации сети доступа (ДЭСД) 10.189.217.19 7142 +Шорабек Дастан Усенулы Кызылорда Кызылординский ДЭСД 10.8.29.72 7242 +Шорабек Дастан Усенулы Лисаковск Костанаиский ДЭСД 10.189.217.20 71433 +Шорабек Дастан Усенулы Рудный Костанайский ДЭСД 10.189.217.21 71431 +Шорабек Дастан Усенулы Шымкент Южно Казахстанский ДЭСД 10.8.29.70 7252 \ No newline at end of file