Initial commit: TopSysOps site + admin panel

Multi-page public site (Next.js), Node/Express backend with Postgres,
and Vite admin/account app, restructured from a single-page layout
into dedicated service, promotions, cases, about, FAQ, and blog pages
for SEO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-08-25 23:12:16 +03:00
commit 130c5382cd
108 files changed
+7146

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
CREATE TABLE IF NOT EXISTS admin_users (
id SERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS service_categories (
id SERIAL PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
description TEXT DEFAULT '',
icon TEXT DEFAULT 'Server',
sort_order INT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS service_items (
id SERIAL PRIMARY KEY,
category_id INT NOT NULL REFERENCES service_categories(id) ON DELETE CASCADE,
name TEXT NOT NULL,
price_text TEXT NOT NULL,
period TEXT DEFAULT '',
note TEXT DEFAULT '',
sort_order INT NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE IF NOT EXISTS promotions (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
price_text TEXT NOT NULL,
old_price_text TEXT,
description TEXT DEFAULT '',
active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS reviews (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
role TEXT,
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
text TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS visits (
date DATE PRIMARY KEY,
count INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_service_items_category ON service_items(category_id);
CREATE INDEX IF NOT EXISTS idx_reviews_status ON reviews(status);
+18
View File
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS cases (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
tag TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'в работе',
tone TEXT NOT NULL DEFAULT 'warn' CHECK (tone IN ('ok', 'warn', 'danger', 'neutral')),
description TEXT DEFAULT '',
sort_order INT NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE IF NOT EXISTS faqs (
id SERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT true
);
+19
View File
@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
company TEXT,
phone TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS support_messages (
id SERIAL PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
sender TEXT NOT NULL CHECK (sender IN ('customer', 'staff')),
body TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_support_messages_customer ON support_messages(customer_id);
+9
View File
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS contact_info (
id INT PRIMARY KEY DEFAULT 1,
address TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
hours TEXT NOT NULL DEFAULT '',
map_embed TEXT NOT NULL DEFAULT '',
CHECK (id = 1)
);
+13
View File
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS blog_posts (
id SERIAL PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
excerpt TEXT DEFAULT '',
content TEXT NOT NULL DEFAULT '',
published BOOLEAN NOT NULL DEFAULT false,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_blog_posts_published ON blog_posts(published, published_at);
@@ -0,0 +1,55 @@
-- Обновление прайс-листа: убирает старую заглушку "Разработка сайтов" (от 5 000 ₽)
-- и добавляет новую линейку "Разработка под ключ" + "White-label и партнёрство".
-- Это настоящая миграция (не разовый скрипт) - применяется САМА при следующем
-- запуске бэкенда (docker compose up -d --build), вручную ничего запускать не нужно.
-- Безопасна к повторному запуску (WHERE NOT EXISTS), но система миграций и так
-- гарантирует, что этот файл выполнится только один раз.
BEGIN;
-- 1) Убираем старую заглушку "Разработка сайтов" (каскадно удалит её позицию "Лендинг")
DELETE FROM service_categories WHERE slug = 'web';
-- 2) Новая категория: Разработка под ключ
INSERT INTO service_categories (slug, title, description, icon, sort_order)
SELECT 'development', 'Разработка под ключ',
'От быстрого MVP до продакшн-разработки и сопровождения', 'Rocket',
(SELECT COALESCE(MAX(sort_order), -1) + 1 FROM service_categories)
WHERE NOT EXISTS (SELECT 1 FROM service_categories WHERE slug = 'development');
INSERT INTO service_items (category_id, name, price_text, period, note, sort_order)
SELECT (SELECT id FROM service_categories WHERE slug = 'development'),
v.name, v.price_text, v.period, v.note, v.sort_order
FROM (VALUES
('MVP «Быстрый старт»', 'от 120 000 ₽', '', 'Посадочная страница, форма/бот для заявок, простая админка - 2-3 недели', 0),
('MVP Pro', 'от 450 000 ₽', '', 'Личный кабинет с авторизацией, оплата, ИИ-ассистент - 4-6 недель', 1),
('Продакшн-разработка', 'от 1 200 000 ₽', '', 'Архитектура, тесты, DevOps, масштабирование - от 2 месяцев, точная смета после дискавери', 2),
('Discovery-воркшоп', 'от 30 000 ₽', '', 'Фиксация ТЗ и оценки перед стартом проекта - 1-2 дня', 3),
('Сопровождение проекта', 'от 40 000 ₽', '/мес', 'Ежемесячные доработки уже готовых продуктов', 4)
) AS v(name, price_text, period, note, sort_order)
WHERE NOT EXISTS (
SELECT 1 FROM service_items
WHERE category_id = (SELECT id FROM service_categories WHERE slug = 'development') AND name = v.name
);
-- 3) Новая категория: White-label и партнёрство
INSERT INTO service_categories (slug, title, description, icon, sort_order)
SELECT 'partners', 'White-label и партнёрство',
'Готовые решения для других компаний и программы сотрудничества', 'Handshake',
(SELECT COALESCE(MAX(sort_order), -1) + 1 FROM service_categories)
WHERE NOT EXISTS (SELECT 1 FROM service_categories WHERE slug = 'partners');
INSERT INTO service_items (category_id, name, price_text, period, note, sort_order)
SELECT (SELECT id FROM service_categories WHERE slug = 'partners'),
v.name, v.price_text, v.period, v.note, v.sort_order
FROM (VALUES
('White-label «Кабинет + ИИ-ассистент»', 'от 500 000 ₽', '', 'Личный кабинет с авторизацией и ИИ-чатом под брендом вашей компании', 0),
('IT-мониторинг сайта', 'от 10 000 ₽', '/мес', 'Аптайм, скорость загрузки, уведомления об инцидентах', 1),
('Реферальная программа', '10%', 'от суммы контракта', 'Бонус вам за рекомендацию нас новому клиенту', 2),
('Партнёрская программа', 'по договорённости', '', 'Комиссия для хостинг-провайдеров и поставщиков оборудования за рекомендации', 3)
) AS v(name, price_text, period, note, sort_order)
WHERE NOT EXISTS (
SELECT 1 FROM service_items
WHERE category_id = (SELECT id FROM service_categories WHERE slug = 'partners') AND name = v.name
);
COMMIT;
+20
View File
@@ -0,0 +1,20 @@
-- Разводим простую посадочную страницу и полноценный MVP по разным ценам:
-- "Лендинг" - без бота-формы и админки, просто сайт;
-- "MVP «Быстрый старт»" (добавлен в 006) - с ботом/формой заявок и админкой.
-- Автоматически применяется при следующем запуске бэкенда, вручную запускать
-- ничего не нужно. Безопасна к повторному запуску (WHERE NOT EXISTS).
INSERT INTO service_items (category_id, name, price_text, period, note, sort_order)
SELECT
(SELECT id FROM service_categories WHERE slug = 'development'),
'Лендинг',
'от 20 000 ₽',
'',
'Простая посадочная страница под продукт или услугу - без формы-бота и админки, 1 неделя',
-1
WHERE EXISTS (SELECT 1 FROM service_categories WHERE slug = 'development')
AND NOT EXISTS (
SELECT 1 FROM service_items
WHERE category_id = (SELECT id FROM service_categories WHERE slug = 'development')
AND name = 'Лендинг'
);
@@ -0,0 +1,38 @@
-- Разводит пересекающиеся предложения по почтовым серверам (акция vs услуга) -
-- явно показывает разницу в составе, и добавляет ориентировочные показатели
-- (сроки/% улучшения/объём) в описания кейсов, с пометкой, что цифры оценочные
-- и уточняются по каждому проекту отдельно.
-- Автоматически применяется при следующем запуске бэкенда, вручную запускать
-- ничего не нужно. UPDATE-ы безопасны к повторному запуску.
BEGIN;
-- 1) Услуги "Почтовый сервер" - уточняем, что НЕ входит (в отличие от акции)
UPDATE service_items
SET note = 'Развёртывание с нуля: установка, домены и почтовые ящики. Без SPF/DKIM/DMARC, антиспама и веб-почты - это в акции «Почтовый сервер под ключ»'
WHERE category_id = (SELECT id FROM service_categories WHERE slug = 'network')
AND name = 'Почтовый сервер (ваше оборудование)';
UPDATE service_items
SET note = 'Аренда сервера + ежемесячное администрирование почты. Разовая настройка SPF/DKIM/DMARC и антиспама - отдельно или по акции «Почтовый сервер под ключ»'
WHERE category_id = (SELECT id FROM service_categories WHERE slug = 'network')
AND name = 'Почтовый сервер (наше оборудование)';
-- 2) Акция "Почтовый сервер под ключ" - явно перечисляем полный состав пакета
UPDATE promotions
SET description = 'Разовая настройка на вашем оборудовании: SPF/DKIM/DMARC, антиспам, веб-почта и 3 месяца сопровождения - всё включено в стоимость по акционной цене'
WHERE title = 'Почтовый сервер под ключ';
-- 3) Кейсы - добавляем ориентировочные цифры (сроки/объём/% улучшения)
UPDATE cases
SET description = 'Перенесли серверы 12 магазинов на новое оборудование без остановки продаж за ~3 недели, снизили время отклика сети на ~30%* (*оценка, уточняется с клиентом по итогам проекта).'
WHERE title = 'Миграция инфраструктуры розничной сети';
UPDATE cases
SET description = 'Внедрили Telegram-бота с RAG на базе базы знаний компании за ~4 недели - снизили нагрузку на операторов поддержки на ~40%* по числу типовых обращений (*оценка, уточняется с клиентом).'
WHERE title = 'ИИ-ассистент для отдела поддержки';
UPDATE cases
SET description = 'Проводим комплексный аудит защищённости и внедряем систему мониторинга угроз 24/7, срок проекта - ориентировочно 6-8 недель* (*уточняется по ходу работ).'
WHERE title = 'Аудит кибербезопасности финтех-проекта';
COMMIT;
@@ -0,0 +1,140 @@
-- Первые статьи блога под конкретные коммерческие направления (IT-аутсорсинг,
-- почтовые серверы, кибербезопасность, ИИ-автоматизация). Автоматически
-- применяется при следующем запуске бэкенда, вручную запускать не нужно.
-- Безопасна к повторному запуску (ON CONFLICT DO NOTHING по slug).
BEGIN;
INSERT INTO blog_posts (slug, title, excerpt, content, published, published_at)
VALUES (
'it-autsorsing-kak-vybrat-podryadchika',
'Как выбрать IT-аутсорсинг для бизнеса: 7 критериев',
'На что смотреть при выборе подрядчика на IT-аутсорсинг, чтобы не переплатить и не остаться без поддержки в критический момент.',
$html$
<p>Передача IT-инфраструктуры на аутсорсинг снимает с бизнеса часть операционной нагрузки, но выбор подрядчика легко превращается в лотерею. Вот на что стоит смотреть до подписания договора.</p>
<h2>1. Время реакции на инциденты закреплено в договоре</h2>
<p>«Быстро отреагируем» - не показатель. Спрашивайте конкретное время реакции по каждому уровню критичности инцидента и убедитесь, что оно зафиксировано в SLA, а не устно.</p>
<h2>2. Есть разные тарифы под разную критичность бизнеса</h2>
<p>Интернет-магазину с пиковыми нагрузками нужна поддержка 24/7, а небольшому офису может хватить графика 5/2. Хороший подрядчик предлагает несколько уровней тарифа, а не один пакет «для всех».</p>
<h2>3. NDA и защищённый доступ к инфраструктуре - по умолчанию</h2>
<p>Подрядчик получает доступ к серверам, базам данных и переписке. Подписание NDA и использование защищённых каналов доступа (а не паролей в мессенджере) должно быть стандартной практикой, а не опцией по запросу.</p>
<h2>4. Прозрачная отчётность о состоянии инфраструктуры</h2>
<p>Вы должны понимать, что происходит с вашими серверами, даже не разбираясь в администрировании глубоко: сколько было инцидентов, что чинили, какие риски видят специалисты.</p>
<h2>5. Возможность разовых работ без абонемента</h2>
<p>Не всегда нужен полный абонемент - иногда достаточно разовой настройки сервера или аудита. Гибкость в этом вопросе часто говорит о зрелости подрядчика больше, чем маркетинговые обещания.</p>
<h2>6. Выезд специалиста - если работаете не полностью удалённо</h2>
<p>Даже при удалённой поддержке иногда нужен физический доступ к оборудованию: замена комплектующих, работа с сетевым железом. Уточняйте регион выезда и сроки реакции по выездным заявкам отдельно от удалённых.</p>
<h2>7. Референсы и живые кейсы, а не только логотипы клиентов</h2>
<p>Логотип на сайте ничего не доказывает. Спрашивайте, какие задачи решали для похожего на ваш бизнеса и какие результаты получили - сроки, объём работ, что изменилось.</p>
<p>Мы в TopSysOps работаем именно по такой модели: несколько тарифов IT-аутсорсинга от базовой удалённой поддержки до 24/7, NDA перед стартом и подробный <a href="/uslugi/support">прайс-лист на техподдержку</a>. Если сомневаетесь, с чего начать - <a href="/kontakty">напишите нам</a>, обсудим вашу инфраструктуру бесплатно.</p>
$html$,
true,
now()
)
ON CONFLICT (slug) DO NOTHING;
INSERT INTO blog_posts (slug, title, excerpt, content, published, published_at)
VALUES (
'nastroyka-pochtovogo-servera-spf-dkim-dmarc',
'Настройка корпоративной почты: SPF, DKIM, DMARC и защита от спама - чек-лист',
'Разбираем, зачем нужны SPF, DKIM и DMARC для корпоративной почты и как проверить, что ваш домен не помечают спамом.',
$html$
<p>Если письма от вашей компании попадают в спам или, того хуже, кто-то отправляет фишинг от имени вашего домена - вероятно, у почтового сервера не настроены базовые механизмы защиты. Разбираем, что это такое и зачем нужно.</p>
<h2>SPF - кто имеет право отправлять почту от вашего домена</h2>
<p>SPF (Sender Policy Framework) - это запись в DNS, которая перечисляет серверы, которым разрешено отправлять письма от имени вашего домена. Без неё любой сервер в мире технически может подделать письмо «от вас».</p>
<h2>DKIM - цифровая подпись письма</h2>
<p>DKIM добавляет к каждому письму криптографическую подпись, которую можно проверить на стороне получателя. Это подтверждает, что письмо не было изменено по дороге и действительно отправлено с вашего сервера.</p>
<h2>DMARC - что делать с письмами, которые не прошли проверку</h2>
<p>DMARC связывает SPF и DKIM и объясняет почтовым сервисам получателей, что делать с письмом, которое не прошло проверки: пропустить, отправить в спам или отклонить. Также DMARC присылает отчёты о том, кто пытается отправлять письма от вашего домена.</p>
<h2>Антиспам и веб-почта - вторая часть уравнения</h2>
<p>SPF/DKIM/DMARC защищают репутацию домена на исходящих письмах. Отдельно нужен антиспам-фильтр на входящих, чтобы сотрудники не разбирали десятки спам-писем вручную, и веб-почта для доступа к ящику из браузера без настройки почтового клиента на каждом устройстве.</p>
<h2>Как понять, что пора этим заняться</h2>
<ul>
<li>Письма клиентам иногда попадают в папку «Спам»</li>
<li>Были случаи фишинговых писем «от имени» вашей компании</li>
<li>Почтовый сервер настраивался разово несколько лет назад и с тех пор не пересматривался</li>
<li>Нет ни SPF, ни DKIM, ни DMARC записей в DNS вашего домена (это можно быстро проверить любым онлайн-чекером)</li>
</ul>
<p>У нас есть отдельная услуга по <a href="/uslugi/network">развёртыванию и администрированию почтовых серверов</a>, а на комплексную настройку с SPF/DKIM/DMARC, антиспамом, веб-почтой и тремя месяцами сопровождения действует акция «Почтовый сервер под ключ» - подробности на <a href="/aktsii">странице акций</a>.</p>
$html$,
true,
now()
)
ON CONFLICT (slug) DO NOTHING;
INSERT INTO blog_posts (slug, title, excerpt, content, published, published_at)
VALUES (
'kiberbezopasnost-malogo-biznesa-s-chego-nachat',
'Кибербезопасность малого и среднего бизнеса: с чего начать',
'Небольшой компании не нужен SOC на 20 человек - но базовый набор мер по кибербезопасности нужен любому бизнесу с серверами и клиентскими данными.',
$html$
<p>Кибербезопасность часто воспринимается как что-то нужное только банкам и крупным корпорациям. На практике небольшая компания - удобная мишень именно потому, что базовые меры защиты у неё, как правило, не настроены.</p>
<h2>1. Аудит - прежде чем что-то внедрять</h2>
<p>Нельзя защитить то, о чём не знаешь. Первый шаг - аудит текущей инфраструктуры: какие сервисы смотрят наружу, какие пароли и доступы используются, где хранятся резервные копии и кто имеет к ним доступ.</p>
<h2>2. Мониторинг вместо разовой проверки</h2>
<p>Аудит - это снимок на конкретный момент. Угрозы появляются постоянно, поэтому кроме разового аудита нужен регулярный мониторинг: логи, алерты о подозрительной активности, обновления уязвимого ПО.</p>
<h2>3. IDS/IPS - обнаружение и предотвращение вторжений</h2>
<p>Системы обнаружения (IDS) и предотвращения (IPS) вторжений отслеживают трафик на предмет подозрительных паттернов - сканирование портов, попытки перебора паролей, известные сигнатуры атак - и либо оповещают, либо блокируют их автоматически.</p>
<h2>4. Резервное копирование, отделённое от основной инфраструктуры</h2>
<p>Один из самых частых сценариев атаки - шифровальщик, который уничтожает и рабочие данные, и бэкапы, если они лежат в одном месте. Резервные копии должны быть изолированы и регулярно проверяться на возможность восстановления.</p>
<h2>5. NDA и разграничение доступов для подрядчиков</h2>
<p>Если с инфраструктурой работает внешняя команда (в том числе на аутсорсе), убедитесь, что доступы выданы по принципу минимально необходимых прав, а не «админ всем».</p>
<h2>С чего начать конкретно</h2>
<p>Если у вас пока нет ни одного из этих пунктов - начните с аудита. Он покажет, какие меры критичны прямо сейчас, а что можно внедрить постепенно. Мы проводим <a href="/uslugi/security">аудит и мониторинг кибербезопасности</a>, включая внедрение IDS/IPS - можно начать с разового аудита без обязательств по абонементу.</p>
$html$,
true,
now()
)
ON CONFLICT (slug) DO NOTHING;
INSERT INTO blog_posts (slug, title, excerpt, content, published, published_at)
VALUES (
'ii-assistent-dlya-biznesa-kak-vnedrit',
'ИИ-ассистент для бизнеса: как внедрить и не прогореть',
'ИИ-бот для поддержки клиентов или сотрудников - не «просто чат-бот». Разбираем, что действительно нужно для рабочего внедрения.',
$html$
<p>Спрос на «внедрить ИИ» часто формулируется расплывчато: «хотим бота, который отвечает на вопросы». В реальности между таким запросом и рабочим инструментом есть несколько важных шагов.</p>
<h2>RAG, а не просто обёртка над языковой моделью</h2>
<p>Просто подключить готовую модель недостаточно - она не знает деталей вашего бизнеса и может отвечать общими фразами или ошибаться в конкретике. RAG (Retrieval-Augmented Generation) добавляет ассистенту доступ к базе знаний вашей компании: документации, FAQ, истории обращений - и ответы становятся конкретными, а не общими.</p>
<h2>Интеграция с реальными каналами</h2>
<p>Ассистент, до которого нужно отдельно заходить на непонятную страницу, используется гораздо реже, чем бот в Telegram или на сайте, куда клиенты и так пишут. Интеграция с мессенджерами, CRM и внутренними сервисами - то, что определяет, будут ли им пользоваться.</p>
<h2>Где ИИ-ассистент окупается быстрее всего</h2>
<ul>
<li>Ответы на типовые вопросы поддержки - снижает нагрузку на операторов на рутинных обращениях</li>
<li>Первичная квалификация заявок до передачи специалисту</li>
<li>Внутренний ассистент для сотрудников по регламентам и базе знаний компании</li>
</ul>
<h2>Что нужно для старта</h2>
<p>Нужна база знаний (даже неструктурированная - её можно привести в порядок в процессе), понимание, в каком канале живут ваши клиенты или сотрудники, и готовность донастраивать ассистента после запуска: первая версия почти никогда не бывает финальной.</p>
<p>Мы разрабатываем ботов и ИИ-ассистентов с обучением на данных компании и дальнейшим помесячным сопровождением - подробнее на странице <a href="/uslugi/ai">«ИИ и автоматизация»</a>. Если пока не уверены, что именно автоматизировать в первую очередь - <a href="/kontakty">напишите нам</a>, поможем определить приоритеты.</p>
$html$,
true,
now()
)
ON CONFLICT (slug) DO NOTHING;
COMMIT;