v1.7.0: refactor max_bot to flat structure, add VCF+UserModel+NLP history context, portal pages and proxy fixes
- Refactored max_bot from nested packages to flat module structure - Q2: Extended BotUser model (patronymic, email, org, address, vcf_raw, contact_hash, phone_verified, email_verified, last_interaction, total_conversations, total_tickets) - Q2: VCF parser (FN, N, TEL, EMAIL, ORG, ADR), upsert on re-contact, NLP history context (_get_user_history_context -> YandexGPT) - Q1: Broadcast preview modal with 10s confirmation timer - Q3: CSS var(--white)->var(--bg-card), var(--text)->var(--text-primary) - Q4: bot_settings showNotification(), editable max_bot_id - Q5: Webhook secret passthrough via X-Max-Bot-Api-Secret - Masking sensitive keys, dialog_cleared handler, migrate via _add_column_if_not_exists() - Rate limit (asyncio.sleep 0.5 per 10), dead code removed, conv.intent context in contact.py - Portal pages: bot_consent, bot_kb (edit), bot_settings, bot_test, bot_tickets, portal_settings - Tests: 21/21 passing, added test_yandex_gpt.py, test_email_sender.py - Deploy: deploy_full.sh, schema.sql, seed_knowledge_base.sql
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
docs/
|
||||
permissions/
|
||||
uploads/
|
||||
.git/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -3,3 +3,4 @@ SECRET_KEY=change-me-to-random-string
|
||||
SESSION_TTL=3600
|
||||
APP_ENV=development
|
||||
LOG_LEVEL=debug
|
||||
PORTAINER_URL=http://81.177.141.34:9001
|
||||
|
||||
@@ -1,6 +1,63 @@
|
||||
# Changelog — AegisOne Service Portal
|
||||
|
||||
## 1.7.0 (29.05.2026)
|
||||
### Новые функции
|
||||
- **Прокси-роуты для бота:** /api/bot/webhook с пробросом X-Max-Bot-Api-Secret
|
||||
- **Страницы бота:** bot_consent.html (превью + таймер 10 сек), bot_kb.html (✏️ редактирование), bot_settings.html (showNotification, max_bot_id)
|
||||
- **Тосты:** глобальная showNotification() в base.html, CSS-классы .toast-* в service.css
|
||||
- **CSS-переменные:** var(--white)→var(--bg-card), var(--text)→var(--text-primary) во всех шаблонах
|
||||
### Исправления
|
||||
- Проброс secret через webhook-прокси
|
||||
|
||||
## 1.5.5 (23.05.2026)
|
||||
### Новые функции
|
||||
- Этап 4 завершён: все 8 feature-обработчиков (risk_score, sla_status, photo_report, appointment, quality_rate, commercial, my_objects, sla_ticket)
|
||||
- max_notifications.py: notify_operator, notify_level2, notify_client, notify_handoff_escalation, notify_ticket_created
|
||||
- emotion_detector.py + categorizer.py: GPT с fallback на keywords
|
||||
- Handoff-диалоги (/service/bot-settings/handoffs) с polling и кнопкой "Взять в работу"
|
||||
- Уведомления бота (/service/bot-settings/notifications) + mark-read
|
||||
- API: handoffs, notifications, risk-questions (CRUD), tickets (list/get/create/message/status), engineers
|
||||
- Страница "Вопросы риск-инжиниринга" (bot_risk_questions.html) — управление через портал
|
||||
- Страница "Заявки" (bot_tickets.html) — просмотр, создание, детали, сообщения, смена статуса
|
||||
- risk_score.py переписан: чтение вопросов из БД (BotRiskQuestion) вместо хардкода
|
||||
- SQL миграция v1.5.5: CREATE TABLE для bot_notifications, bot_notification_subscriptions, bot_risk_questions, bot_tickets, bot_ticket_messages, bot_ticket_statuses + seed-данные
|
||||
- MaxAPIClient.test_mode — заглушка для тестового прогона без отправки в MAX
|
||||
- /api/bot/test-run — вызывает handle_update с test_mode, симулируя настоящий диалог
|
||||
- portal_settings.html: кнопка тестового прогона с живой симуляцией
|
||||
- role_settings.html: новые permission (bot_tickets, bot_notifications, bot_risk_questions)
|
||||
- Сайдбар: добавлены "Заявки" и "Вопросы риск-инжиниринга" в раздел Чат-бот Max
|
||||
- Пользователи (users.html): колонка MAX ID + поле в форме создания/редактирования
|
||||
### Исправления и улучшения
|
||||
- bot_engine.py: callback `rate_*` → прямой вызов quality_rate handler
|
||||
- bot_engine.py: добавлен вызов notify_handoff_escalation при негативных эмоциях
|
||||
- Users stub: login, full_name, role, is_active, max_user_id
|
||||
- Proxy-роуты py_service: добавлены все API для risk-questions, tickets, engineers
|
||||
- Тесты max_bot: проверка всех роутов
|
||||
- **Добавлена фича "Напоминания"** — reminders.py handler, состояние FEATURE_REMINDERS в FSM, кнопка в меню
|
||||
- **broadcasts.py зарегистрирован** — добавлен в _handle_feature (main_menu.py) и в build_main_menu_buttons (inline.py)
|
||||
- **FSM.transition("done")** — все feature-хендлеры теперь возвращают в MAIN_MENU вместо IDLE, показывают меню
|
||||
- **FSM изоляция** — risk_score, appointment, commercial переписаны с set_temp/get_temp вместо прямого доступа к _states
|
||||
- **settings_cache.py** — исправлены get_phones() (парсит phone_1/phone_2) и get_email() (читает support_email)
|
||||
- **Delete forms** — bot_categories.html, bot_kb.html, bot_settings.html: заменены form POST на fetch() с JSON
|
||||
- **SQL миграция v1.5.5 исправлена** — bot_notifications + recipient_max_user_id/related_*, подписки с boolean-колонками
|
||||
- **Версия обновлена до 1.5.5
|
||||
|
||||
## 1.5.4 (22.05.2026)
|
||||
### Новые функции
|
||||
- Страница Handoff-диалогов (bot_handoffs.html) с polling
|
||||
- Страница Уведомлений бота (bot_notifications.html) с mark-read
|
||||
- API: handoffs, notifications, risk-questions, tickets, engineers (max_bot/main.py)
|
||||
- max_notifications.py: уведомления операторам, level2, клиентам, эскалация
|
||||
- emotion_detector.py + categorizer.py: модули GPT + keyword fallback
|
||||
- risk_score.py: анкета 7 вопросов с in-memory хранением через FSM
|
||||
- Уведомления FSM (notifications.py + broadcasts.py): подписка / отписка
|
||||
- Users stub (login, full_name, role, max_user_id)
|
||||
- Пользователи: колонка MAX ID + поле в форме (users.html)
|
||||
- SQL миграция v1.5.5 (черновик)
|
||||
### Исправления и улучшения
|
||||
- risk_score.py: переключение с хардкода на БД (BotRiskQuestion)
|
||||
- bot_engine.py: маршрутизация feature-состояний через _feature_handlers
|
||||
- bot_engine.py: callback rate_* → прямой вызов handle_quality_rate
|
||||
|
||||
## 1.5.3 (22.05.2026)
|
||||
### Новые функции
|
||||
|
||||
@@ -2,13 +2,8 @@ FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir --only-binary :all: -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import Request, Depends
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi import Request, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
|
||||
def get_current_user(request: Request) -> dict | None:
|
||||
@@ -11,6 +11,6 @@ def require_role(*roles: str):
|
||||
if not user:
|
||||
return RedirectResponse(url="/service/login", status_code=302)
|
||||
if roles and user["role"] not in roles:
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
raise HTTPException(status_code=403, detail="Доступ запрещён")
|
||||
return user
|
||||
return dependency
|
||||
|
||||
+163
-154
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from datetime import datetime
|
||||
from app.config import get_settings
|
||||
from app.services.docs_permissions import load_permissions, save_permissions, discover_docs, can_access_doc, get_allowed_docs
|
||||
from app.dependencies import get_current_user, require_role
|
||||
@@ -15,6 +16,22 @@ router = APIRouter(prefix="/service/documents", tags=["documents"])
|
||||
from app.templating import templates
|
||||
|
||||
|
||||
def now_str():
|
||||
return datetime.now().strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
|
||||
def _doc_ctx(request, user, **kw):
|
||||
original_user = request.session.get("original_user")
|
||||
available_roles = {"owner": ["owner", "engineer", "technician"], "engineer": ["engineer", "technician"]}
|
||||
c = {"request": request, "user": user, "now": now_str(),
|
||||
"role_permissions": getattr(request.state, 'role_permissions', {}),
|
||||
"role_override_active": original_user is not None,
|
||||
"original_role": original_user["role"] if original_user else None,
|
||||
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else []}
|
||||
c.update(kw)
|
||||
return c
|
||||
|
||||
|
||||
def transliterate_slug(text: str) -> str:
|
||||
translit_map = {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh',
|
||||
@@ -41,157 +58,7 @@ async def documents_list(
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
docs = get_allowed_docs(user["role"])
|
||||
return templates.TemplateResponse("documents/list.html", {
|
||||
"request": request, "user": user, "docs": docs,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/{slug}")
|
||||
async def document_view(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if not can_access_doc(user["role"], slug, "view"):
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
if not os.path.exists(filepath):
|
||||
return HTMLResponse(content="Файл не найден", status_code=404)
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
html_content = render_markdown(content)
|
||||
return templates.TemplateResponse("documents/view.html", {
|
||||
"request": request, "user": user, "doc": doc, "content": html_content,
|
||||
"can_edit": can_access_doc(user["role"], slug, "edit"),
|
||||
"can_download": user["role"] == "owner",
|
||||
})
|
||||
|
||||
|
||||
@router.get("/{slug}/edit")
|
||||
async def document_edit(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if not can_access_doc(user["role"], slug, "edit"):
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
if not os.path.exists(filepath):
|
||||
return HTMLResponse(content="Файл не найден", status_code=404)
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
return templates.TemplateResponse("documents/edit.html", {
|
||||
"request": request, "user": user, "doc": doc, "content": content,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/{slug}/edit")
|
||||
async def document_update(
|
||||
slug: str,
|
||||
request: Request,
|
||||
content: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if not can_access_doc(user["role"], slug, "edit"):
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return RedirectResponse(url=f"/service/documents/{slug}", status_code=302)
|
||||
|
||||
|
||||
@router.get("/tech-access")
|
||||
async def tech_access_page(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if user["role"] != "engineer":
|
||||
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
|
||||
|
||||
perms = discover_docs()
|
||||
editable_docs = [
|
||||
doc for slug, doc in perms.items()
|
||||
if doc.get("permissions", {}).get("engineer", {}).get("edit", False)
|
||||
]
|
||||
return templates.TemplateResponse("documents/tech_access.html", {
|
||||
"request": request, "user": user, "docs": editable_docs, "perms": perms,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/tech-access")
|
||||
async def tech_access_update(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if user["role"] != "engineer":
|
||||
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
|
||||
|
||||
form = await request.form()
|
||||
perms = discover_docs()
|
||||
|
||||
for slug in perms.keys():
|
||||
if not perms[slug].get("permissions", {}).get("engineer", {}).get("edit", False):
|
||||
continue
|
||||
technician_view = form.get(f"{slug}_technician_view") == "on"
|
||||
perms[slug]["permissions"]["technician"] = {"view": technician_view}
|
||||
|
||||
save_permissions(perms)
|
||||
return RedirectResponse(url="/service/documents/tech-access", status_code=302)
|
||||
|
||||
|
||||
@router.get("/{slug}/download")
|
||||
async def document_download(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if user["role"] != "owner":
|
||||
return HTMLResponse(content="Скачивание доступно только владельцу", status_code=403)
|
||||
|
||||
from fastapi.responses import FileResponse
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
if not os.path.exists(filepath):
|
||||
return HTMLResponse(content="Файл не найден", status_code=404)
|
||||
|
||||
return FileResponse(filepath, filename=doc["filename"], media_type="text/markdown")
|
||||
return templates.TemplateResponse("documents/list.html", _doc_ctx(request, user, docs=docs))
|
||||
|
||||
|
||||
@router.get("/admin/permissions")
|
||||
@@ -204,9 +71,7 @@ async def permissions_page(
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
perms = discover_docs()
|
||||
return templates.TemplateResponse("documents/permissions.html", {
|
||||
"request": request, "user": user, "perms": perms,
|
||||
})
|
||||
return templates.TemplateResponse("documents/permissions.html", _doc_ctx(request, user, perms=perms))
|
||||
|
||||
|
||||
@router.post("/admin/permissions")
|
||||
@@ -280,3 +145,147 @@ async def document_create(
|
||||
save_permissions(perms)
|
||||
|
||||
return RedirectResponse(url=f"/service/documents/{slug}/edit", status_code=302)
|
||||
|
||||
|
||||
@router.get("/tech-access")
|
||||
async def tech_access_page(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if user["role"] != "engineer":
|
||||
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
|
||||
|
||||
perms = discover_docs()
|
||||
editable_docs = [
|
||||
doc for slug, doc in perms.items()
|
||||
if doc.get("permissions", {}).get("engineer", {}).get("edit", False)
|
||||
]
|
||||
return templates.TemplateResponse("documents/tech_access.html", _doc_ctx(request, user, docs=editable_docs, perms=perms))
|
||||
|
||||
|
||||
@router.post("/tech-access")
|
||||
async def tech_access_update(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if user["role"] != "engineer":
|
||||
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
|
||||
|
||||
form = await request.form()
|
||||
perms = discover_docs()
|
||||
|
||||
for slug in perms.keys():
|
||||
if not perms[slug].get("permissions", {}).get("engineer", {}).get("edit", False):
|
||||
continue
|
||||
technician_view = form.get(f"{slug}_technician_view") == "on"
|
||||
perms[slug]["permissions"]["technician"] = {"view": technician_view}
|
||||
|
||||
save_permissions(perms)
|
||||
return RedirectResponse(url="/service/documents/tech-access", status_code=302)
|
||||
|
||||
|
||||
@router.get("/{slug}")
|
||||
async def document_view(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if not can_access_doc(user["role"], slug, "view"):
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
if not os.path.exists(filepath):
|
||||
return HTMLResponse(content="Файл не найден", status_code=404)
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
html_content = render_markdown(content)
|
||||
return templates.TemplateResponse("documents/view.html", _doc_ctx(request, user,
|
||||
doc=doc, content=html_content,
|
||||
can_edit=can_access_doc(user["role"], slug, "edit"),
|
||||
can_download=user["role"] == "owner",
|
||||
))
|
||||
|
||||
|
||||
@router.get("/{slug}/edit")
|
||||
async def document_edit(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if not can_access_doc(user["role"], slug, "edit"):
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
if not os.path.exists(filepath):
|
||||
return HTMLResponse(content="Файл не найден", status_code=404)
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
return templates.TemplateResponse("documents/edit.html", _doc_ctx(request, user, doc=doc, content=content))
|
||||
|
||||
|
||||
@router.post("/{slug}/edit")
|
||||
async def document_update(
|
||||
slug: str,
|
||||
request: Request,
|
||||
content: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if not can_access_doc(user["role"], slug, "edit"):
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return RedirectResponse(url=f"/service/documents/{slug}", status_code=302)
|
||||
|
||||
|
||||
@router.get("/{slug}/download")
|
||||
async def document_download(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
if user["role"] != "owner":
|
||||
return HTMLResponse(content="Скачивание доступно только владельцу", status_code=403)
|
||||
|
||||
from fastapi.responses import FileResponse
|
||||
docs_dir = settings.DOCS_DIR
|
||||
perms = discover_docs()
|
||||
doc = perms.get(slug)
|
||||
if not doc:
|
||||
return HTMLResponse(content="Документ не найден", status_code=404)
|
||||
|
||||
filepath = os.path.join(docs_dir, doc["filename"])
|
||||
if not os.path.exists(filepath):
|
||||
return HTMLResponse(content="Файл не найден", status_code=404)
|
||||
|
||||
return FileResponse(filepath, filename=doc["filename"], media_type="text/markdown")
|
||||
|
||||
+54
-1
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, Depends
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
@@ -63,7 +64,7 @@ SEED_COEFFICIENTS = [
|
||||
|
||||
async def seed_initial_data():
|
||||
from sqlalchemy import text, select, func
|
||||
from app.models.models import FormulaCoefficient, User
|
||||
from app.models.models import FormulaCoefficient, User, RoleMenuPermission
|
||||
|
||||
async with async_session() as db:
|
||||
try:
|
||||
@@ -74,6 +75,24 @@ async def seed_initial_data():
|
||||
db.add(FC(key=key, value=value, description=desc, formula_ref=ref))
|
||||
await db.commit()
|
||||
|
||||
# Seed default role menu permissions if table is empty
|
||||
perm_count = await db.execute(select(func.count(RoleMenuPermission.id)))
|
||||
if perm_count.scalar() == 0:
|
||||
DEFAULT_PERMISSIONS = {
|
||||
"engineer": [
|
||||
"charts", "customers", "objects", "sla", "questionnaire", "passports",
|
||||
"tasks", "reports", "incidents", "tech_access", "docs",
|
||||
|
||||
],
|
||||
"technician": [
|
||||
"tasks", "reports", "incidents", "checklist",
|
||||
],
|
||||
}
|
||||
for role, keys in DEFAULT_PERMISSIONS.items():
|
||||
for key in keys:
|
||||
db.add(RoleMenuPermission(role=role, menu_key=key, visible=True))
|
||||
await db.commit()
|
||||
|
||||
await db.execute(text("DELETE FROM users WHERE login = 'admin'"))
|
||||
await db.commit()
|
||||
except Exception:
|
||||
@@ -91,6 +110,7 @@ async def lifespan(app: FastAPI):
|
||||
from app.calculations.engine import reload_coefficients
|
||||
await reload_coefficients(db)
|
||||
discover_docs()
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@@ -116,6 +136,32 @@ class RoleGuardMiddleware(BaseHTTPMiddleware):
|
||||
app.add_middleware(RoleGuardMiddleware)
|
||||
app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY, max_age=settings.SESSION_TTL)
|
||||
|
||||
|
||||
class PermissionsMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
if "session" not in request.scope:
|
||||
request.state.role_permissions = {}
|
||||
return await call_next(request)
|
||||
from app.models.models import RoleMenuPermission
|
||||
from sqlalchemy import select
|
||||
user = request.session.get("user")
|
||||
if user and user.get("role") in ("engineer", "technician"):
|
||||
from app.database import async_session
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(RoleMenuPermission).where(RoleMenuPermission.role == user["role"])
|
||||
)
|
||||
request.state.role_permissions = {
|
||||
row.menu_key: row.visible for row in result.scalars().all()
|
||||
}
|
||||
else:
|
||||
request.state.role_permissions = {}
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
app.add_middleware(PermissionsMiddleware)
|
||||
|
||||
app.include_router(documents_router)
|
||||
|
||||
from app.routers.service_pages import router as service_pages_router
|
||||
@@ -210,8 +256,15 @@ async def dashboard(request: Request, db: AsyncSession = Depends(get_db), user:
|
||||
shs_alert = f"SHS ниже 70 ({stats['shs']['shs_score']}) — {stats['shs']['shs_status']}"
|
||||
|
||||
now = __import__("datetime").datetime.now().strftime("%d.%m.%Y %H:%M")
|
||||
original_user = request.session.get("original_user")
|
||||
available_roles = {"owner": ["owner", "engineer", "technician"], "engineer": ["engineer", "technician"]}
|
||||
return templates.TemplateResponse("dashboard.html", {
|
||||
"request": request, "stats": stats, "shs_alert": shs_alert, "now": now, "user": user,
|
||||
"active_page": "dashboard",
|
||||
"role_permissions": getattr(request.state, 'role_permissions', {}),
|
||||
"role_override_active": original_user is not None,
|
||||
"original_role": original_user["role"] if original_user else None,
|
||||
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else [],
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class User(Base):
|
||||
full_name = Column(String(255), nullable=False)
|
||||
phone = Column(String(50), default="")
|
||||
email = Column(String(255), default="")
|
||||
max_user_id = Column(BigInteger, nullable=True)
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
created_at = Column(DateTime, nullable=False, server_default="NOW()")
|
||||
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate="NOW()")
|
||||
@@ -495,3 +496,19 @@ class QuickMenuItem(Base):
|
||||
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate="NOW()")
|
||||
|
||||
creator = relationship("User")
|
||||
|
||||
|
||||
class RoleMenuPermission(Base):
|
||||
__tablename__ = "role_menu_permissions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
role = Column(String(20), nullable=False)
|
||||
menu_key = Column(String(50), nullable=False)
|
||||
visible = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("role IN ('engineer','technician')", name="ck_role_menu_role"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -6,6 +6,7 @@ from fastapi.responses import RedirectResponse, JSONResponse, HTMLResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, delete, or_, and_, Integer, text
|
||||
from sqlalchemy.orm import selectinload, joinedload
|
||||
import httpx
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user, require_role
|
||||
from app.auth import audit_log
|
||||
@@ -1135,45 +1136,6 @@ async def cases_delete(request: Request, db: AsyncSession = Depends(get_db), use
|
||||
return RedirectResponse(url="/service/cases", status_code=302)
|
||||
|
||||
|
||||
# ===================== BOT SETTINGS =====================
|
||||
|
||||
@router.get("/service/bot-settings")
|
||||
async def bot_settings(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_settings.html", ctx(request, user=user, active_page="bot-settings", title="Настройки бота"))
|
||||
|
||||
@router.get("/service/bot-settings/responses")
|
||||
async def bot_responses(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_responses.html", ctx(request, user=user, active_page="bot-responses", title="Шаблоны ответов бота"))
|
||||
|
||||
@router.get("/service/bot-settings/features")
|
||||
async def bot_features(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_features.html", ctx(request, user=user, active_page="bot-features", title="Функции бота"))
|
||||
|
||||
@router.get("/service/bot-settings/categories")
|
||||
async def bot_categories(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_categories.html", ctx(request, user=user, active_page="bot-categories", title="Категории обращений"))
|
||||
|
||||
@router.get("/service/bot-settings/kb")
|
||||
async def bot_kb(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_kb.html", ctx(request, user=user, active_page="bot-kb", title="База знаний бота"))
|
||||
|
||||
@router.get("/service/bot-settings/conversations")
|
||||
async def bot_conversations(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_conversations.html", ctx(request, user=user, active_page="bot-conversations", title="Диалоги бота"))
|
||||
|
||||
@router.get("/service/bot-settings/users")
|
||||
async def bot_users(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_users.html", ctx(request, user=user, active_page="bot-users", title="Пользователи бота"))
|
||||
|
||||
@router.get("/service/bot-settings/test-runner")
|
||||
async def bot_test_runner(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_test_runner.html", ctx(request, user=user, active_page="bot-test-runner", title="Тестовый прогон бота"))
|
||||
|
||||
@router.get("/service/bot-settings/analytics")
|
||||
async def bot_analytics(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_analytics.html", ctx(request, user=user, active_page="bot-analytics", title="Аналитика бота"))
|
||||
|
||||
|
||||
@router.get("/service/portal-settings")
|
||||
async def portal_settings_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/portal_settings.html", ctx(request, user=user, active_page="portal-settings", title="Настройка портала"))
|
||||
@@ -1184,6 +1146,40 @@ async def role_settings_page(request: Request, db: AsyncSession = Depends(get_db
|
||||
return templates.TemplateResponse("pages/role_settings.html", ctx(request, user=user, active_page="role-settings", title="Настройка Ролей"))
|
||||
|
||||
|
||||
# ===================== CHAT-BOT MAX PAGES =====================
|
||||
|
||||
@router.get("/service/bot-settings")
|
||||
async def bot_settings_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
|
||||
return templates.TemplateResponse("pages/bot_settings.html", ctx(request, user=user, active_page="bot-settings", title="Настройки чат-бота MAX"))
|
||||
|
||||
|
||||
@router.get("/service/bot-kb")
|
||||
async def bot_kb_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
|
||||
return templates.TemplateResponse("pages/bot_kb.html", ctx(request, user=user, active_page="bot-kb", title="База знаний чат-бота"))
|
||||
|
||||
|
||||
@router.get("/service/bot-test")
|
||||
async def bot_test_page_redirect():
|
||||
return RedirectResponse(url="/service/bot-settings", status_code=302)
|
||||
|
||||
|
||||
@router.get("/service/bot-test-content")
|
||||
async def bot_test_content(request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return HTMLResponse("Not authenticated")
|
||||
return templates.TemplateResponse("pages/bot_test.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/service/bot-consent")
|
||||
async def bot_consent_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
|
||||
return templates.TemplateResponse("pages/bot_consent.html", ctx(request, user=user, active_page="bot-consent", title="Согласия ФЗ-152 и рассылка"))
|
||||
|
||||
|
||||
@router.get("/service/bot-tickets")
|
||||
async def bot_tickets_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
|
||||
return templates.TemplateResponse("pages/bot_tickets.html", ctx(request, user=user, active_page="bot-tickets", title="Заявки от чат-бота"))
|
||||
|
||||
|
||||
# ---- Role permissions API ----
|
||||
@router.get("/service/api/role-permissions")
|
||||
async def api_role_permissions_get(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
@@ -1242,332 +1238,111 @@ async def api_role_reset(request: Request, user: dict = Depends(get_current_user
|
||||
return JSONResponse({"ok": True, "role": request.session["user"]["role"]})
|
||||
|
||||
|
||||
# ===================== BOT API (proxied from max_bot) =====================
|
||||
# ===================== BOT API PROXY =====================
|
||||
BOT_SERVICE_URL = "http://127.0.0.1:8002"
|
||||
|
||||
import aiohttp
|
||||
|
||||
MAX_BOT_URL = "http://127.0.0.1:8002"
|
||||
|
||||
async def _proxy(method, path, json_body=None):
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
|
||||
async with session.request(method, f"{MAX_BOT_URL}{path}", json=json_body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
except aiohttp.ClientError as e:
|
||||
return JSONResponse({"error": f"Bot service unavailable: {e}", "ok": False}, status_code=503)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e), "ok": False}, status_code=500)
|
||||
async def _proxy(method: str, path: str, body=None):
|
||||
async with httpx.AsyncClient() as client:
|
||||
if body is not None:
|
||||
resp = await client.request(method, f"{BOT_SERVICE_URL}{path}", json=body)
|
||||
else:
|
||||
resp = await client.request(method, f"{BOT_SERVICE_URL}{path}")
|
||||
return JSONResponse(resp.json(), status_code=resp.status_code)
|
||||
|
||||
|
||||
@router.get("/service/api/bot/settings")
|
||||
async def api_bot_settings(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async def proxy_bot_settings_get(user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
return await _proxy("GET", "/api/bot/settings")
|
||||
|
||||
|
||||
@router.post("/service/api/bot/settings/save")
|
||||
async def api_bot_settings_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async def proxy_bot_settings_save(request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/settings/save", body)
|
||||
|
||||
@router.get("/service/api/bot/templates")
|
||||
async def api_bot_templates(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/templates")
|
||||
|
||||
@router.post("/service/api/bot/templates/save")
|
||||
async def api_bot_templates_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/templates/save", body)
|
||||
|
||||
@router.get("/service/api/bot/features")
|
||||
async def api_bot_features(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/features")
|
||||
|
||||
@router.post("/service/api/bot/features/toggle")
|
||||
async def api_bot_features_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/features/toggle", body)
|
||||
|
||||
@router.get("/service/api/bot/categories")
|
||||
async def api_bot_categories(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/categories")
|
||||
|
||||
@router.post("/service/api/bot/categories/create")
|
||||
async def api_bot_categories_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/categories/create", body)
|
||||
|
||||
@router.post("/service/api/bot/categories/edit")
|
||||
async def api_bot_categories_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/categories/edit", body)
|
||||
|
||||
@router.post("/service/api/bot/categories/delete")
|
||||
async def api_bot_categories_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/categories/delete", body)
|
||||
|
||||
@router.get("/service/api/bot/kb")
|
||||
async def api_bot_kb(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
@router.get("/api/bot/kb")
|
||||
async def proxy_bot_kb_list(user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
return await _proxy("GET", "/api/bot/kb")
|
||||
|
||||
@router.post("/service/api/bot/kb/create")
|
||||
async def api_bot_kb_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
|
||||
@router.post("/api/bot/kb")
|
||||
async def proxy_bot_kb_create(request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/kb/create", body)
|
||||
return await _proxy("POST", "/api/bot/kb", body)
|
||||
|
||||
@router.post("/service/api/bot/kb/edit")
|
||||
async def api_bot_kb_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
|
||||
@router.delete("/api/bot/kb/{card_id}")
|
||||
async def proxy_bot_kb_delete(card_id: int, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
return await _proxy("DELETE", f"/api/bot/kb/{card_id}")
|
||||
|
||||
|
||||
@router.get("/api/bot/tickets")
|
||||
async def proxy_bot_tickets_list(user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
return await _proxy("GET", "/api/bot/tickets")
|
||||
|
||||
|
||||
@router.patch("/api/bot/tickets/{ticket_id}")
|
||||
async def proxy_bot_tickets_update(ticket_id: int, request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/kb/edit", body)
|
||||
return await _proxy("PATCH", f"/api/bot/tickets/{ticket_id}", body)
|
||||
|
||||
@router.post("/service/api/bot/kb/delete")
|
||||
async def api_bot_kb_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
|
||||
@router.put("/api/bot/kb/{card_id}")
|
||||
async def proxy_bot_kb_update(card_id: int, request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/kb/delete", body)
|
||||
return await _proxy("PUT", f"/api/bot/kb/{card_id}", body)
|
||||
|
||||
@router.get("/service/api/bot/conversations")
|
||||
async def api_bot_conversations(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/conversations")
|
||||
|
||||
@router.get("/service/api/bot/users")
|
||||
async def api_bot_users(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
page: int = Query(1), limit: int = Query(20),
|
||||
sort_by: str = Query("created_at"), sort_dir: str = Query("desc"),
|
||||
consent: str = Query(None)):
|
||||
params = f"?page={page}&limit={limit}&sort_by={sort_by}&sort_dir={sort_dir}"
|
||||
if consent:
|
||||
params += f"&consent={consent}"
|
||||
return await _proxy("GET", f"/api/bot/users{params}")
|
||||
@router.get("/api/bot/users/consented")
|
||||
async def proxy_bot_users_consented(user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
return await _proxy("GET", "/api/bot/users/consented")
|
||||
|
||||
@router.get("/service/api/bot/analytics")
|
||||
async def api_bot_analytics(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/analytics")
|
||||
|
||||
@router.post("/service/api/bot/test-run")
|
||||
async def api_bot_test_run(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/test-run", body)
|
||||
|
||||
@router.get("/service/api/bot/broadcast/recipients")
|
||||
async def api_bot_broadcast_recipients(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
q: str = Query(None), page: int = Query(1), limit: int = Query(200)):
|
||||
params = f"?page={page}&limit={limit}"
|
||||
if q:
|
||||
params += f"&q={q}"
|
||||
return await _proxy("GET", f"/api/bot/broadcast/recipients{params}")
|
||||
|
||||
@router.get("/service/api/bot/broadcast/history")
|
||||
async def api_bot_broadcast_history(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
page: int = Query(1), limit: int = Query(20)):
|
||||
return await _proxy("GET", f"/api/bot/broadcast/history?page={page}&limit={limit}")
|
||||
|
||||
@router.post("/service/api/bot/broadcast")
|
||||
async def api_bot_broadcast(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
@router.post("/api/bot/broadcast")
|
||||
async def proxy_bot_broadcast(request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/broadcast", body)
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/handoffs")
|
||||
async def bot_handoffs_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_handoffs.html", ctx(request, user=user, active_page="bot-handoffs", title="Handoff-диалоги"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/handoffs/poll")
|
||||
async def bot_handoffs_poll(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/handoffs")
|
||||
|
||||
|
||||
@router.post("/service/bot-settings/handoffs/{handoff_id}/take")
|
||||
async def bot_handoff_take(handoff_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/take")
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/notifications")
|
||||
async def bot_notifications_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_notifications.html", ctx(request, user=user, active_page="bot-notifications", title="Уведомления бота"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/notifications/data")
|
||||
async def bot_notifications_data(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/notifications")
|
||||
|
||||
|
||||
@router.post("/service/bot-settings/notifications/{notif_id}/read")
|
||||
async def bot_notification_read(notif_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("POST", f"/api/bot/notifications/{notif_id}/read")
|
||||
|
||||
|
||||
# ===================== BOT RISK QUESTIONS =====================
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/risk-questions")
|
||||
async def bot_risk_questions_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_risk_questions.html", ctx(request, user=user, active_page="bot-risk-questions", title="Вопросы риск-инжиниринга"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/storage")
|
||||
async def bot_storage_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_storage.html", ctx(request, user=user, active_page="bot-storage", title="Яндекс.Диск"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/broadcasts")
|
||||
async def bot_broadcasts_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_broadcasts.html", ctx(request, user=user, active_page="bot-broadcasts", title="Рассылки"))
|
||||
|
||||
|
||||
@router.get("/service/api/bot/risk-questions")
|
||||
async def api_bot_risk_questions(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/risk-questions")
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/create")
|
||||
async def api_bot_risk_questions_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
@router.post("/api/bot/broadcast/preview")
|
||||
async def proxy_bot_broadcast_preview(request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/create", body)
|
||||
return await _proxy("POST", "/api/bot/broadcast/preview", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/edit")
|
||||
async def api_bot_risk_questions_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
@router.post("/api/bot/webhook")
|
||||
async def proxy_bot_webhook(request: Request):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/edit", body)
|
||||
headers = {"Content-Type": "application/json"}
|
||||
secret = request.headers.get("X-Max-Bot-Api-Secret")
|
||||
if secret:
|
||||
headers["X-Max-Bot-Api-Secret"] = secret
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(f"{BOT_SERVICE_URL}/webhook", json=body, headers=headers)
|
||||
return JSONResponse(resp.json(), status_code=resp.status_code)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/toggle")
|
||||
async def api_bot_risk_questions_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/toggle", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/delete")
|
||||
async def api_bot_risk_questions_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/delete", body)
|
||||
|
||||
|
||||
# ===================== BOT TICKETS =====================
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/tickets")
|
||||
async def bot_tickets_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_tickets.html", ctx(request, user=user, active_page="bot-tickets", title="Заявки"))
|
||||
|
||||
|
||||
@router.get("/service/api/bot/tickets")
|
||||
async def api_bot_tickets(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
status: str = Query(None), priority: str = Query(None)):
|
||||
params = ""
|
||||
if status: params += f"status={status}&"
|
||||
if priority: params += f"priority={priority}"
|
||||
return await _proxy("GET", f"/api/bot/tickets?{params}")
|
||||
|
||||
|
||||
@router.get("/service/api/bot/tickets/{ticket_id}")
|
||||
async def api_bot_ticket_get(ticket_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", f"/api/bot/tickets/{ticket_id}")
|
||||
|
||||
|
||||
@router.post("/service/api/bot/tickets/create")
|
||||
async def api_bot_tickets_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/tickets/create", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/tickets/{ticket_id}/message")
|
||||
async def api_bot_tickets_message(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/message", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/tickets/{ticket_id}/status")
|
||||
async def api_bot_tickets_status(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/status", body)
|
||||
|
||||
|
||||
@router.get("/service/api/bot/engineers")
|
||||
async def api_bot_engineers(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/engineers")
|
||||
|
||||
|
||||
# ===================== BOT STORAGE (Yandex Disk) =====================
|
||||
|
||||
async def _get_yandex_client(db: AsyncSession):
|
||||
from app.yandex_disk import YandexDiskClient
|
||||
result = await db.execute(text("SELECT value FROM bot_settings WHERE key = 'yandex_disk_token'"))
|
||||
row = result.scalar_one_or_none()
|
||||
if not row:
|
||||
return None
|
||||
return YandexDiskClient(row)
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/list")
|
||||
async def api_bot_storage_list(path: str = Query(""), db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
items = await client.list_folder(path)
|
||||
return JSONResponse(items)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/search")
|
||||
async def api_bot_storage_search(query: str = Query(""), db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
items = await client.search(query)
|
||||
return JSONResponse(items)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/public-link")
|
||||
async def api_bot_storage_public_link(path: str = Query(""), expiry: int = Query(30),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
url = await client.get_public_url(path, expiry)
|
||||
return JSONResponse({"url": url})
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/download")
|
||||
async def api_bot_storage_download(path: str = Query(""), db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
info = await client.get_file_info(path)
|
||||
dl_url = info.get("public_url", "")
|
||||
if not dl_url:
|
||||
dl_info = await client._request("GET", "/resources/download", params={"path": path})
|
||||
dl_url = dl_info.get("href", "")
|
||||
return JSONResponse({"url": dl_url})
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
# ===================== HANDOFF CLOSE =====================
|
||||
|
||||
|
||||
@router.post("/service/api/bot/handoffs/{handoff_id}/close")
|
||||
async def api_bot_handoff_close(handoff_id: int, db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/close")
|
||||
@@ -98,11 +98,7 @@ body {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
/* EasyMDE dark theme overrides */
|
||||
[data-theme="dark"] .EasyMDEContainer .editor-toolbar {
|
||||
|
||||
@@ -52,8 +52,14 @@ img { max-width:100%; height:auto; }
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
[data-theme="dark"] .login-page {
|
||||
background: linear-gradient(135deg, #0a0e1a 0%, #0d1a30 50%, #0a0e1a 100%);
|
||||
}
|
||||
[data-theme="light"] .login-page {
|
||||
background: linear-gradient(135deg, #e8ecf1 0%, #f0f4f8 50%, #e8ecf1 100%);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: var(--bg-card);
|
||||
@@ -965,6 +971,72 @@ table .actions button {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
/* ---------- ROLE SWITCHER ---------- */
|
||||
.role-switcher {
|
||||
position: relative;
|
||||
}
|
||||
.role-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--accent-dim);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.role-btn:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.role-btn-label { font-weight: 600; text-transform: capitalize; }
|
||||
.role-btn-arrow { font-size: 10px; }
|
||||
.role-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
min-width: 160px;
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
}
|
||||
.role-dropdown.open { display: block; }
|
||||
.role-dropdown-item {
|
||||
display: block;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.role-dropdown-item:hover { background: var(--accent-dim); }
|
||||
.role-dropdown-item.active { font-weight: 700; color: var(--accent); }
|
||||
.role-dropdown-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
.role-dropdown-reset { color: var(--danger); }
|
||||
.role-dropdown-reset:hover { background: rgba(239,68,68,0.1) !important; }
|
||||
|
||||
/* ---------- USER NAME ---------- */
|
||||
.navbar__user {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---------- DOCUMENTS ---------- */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
(function() {
|
||||
var ROLE_NAMES = { owner: 'Владелец', engineer: 'Инженер', technician: 'Техник' };
|
||||
var EMOJIS = { owner: '👑', engineer: '🔧', technician: '🛠️' };
|
||||
|
||||
function closeDropdown() {
|
||||
var dd = document.querySelector('.role-dropdown');
|
||||
if (dd) dd.classList.remove('open');
|
||||
}
|
||||
|
||||
function switchRole(role) {
|
||||
closeDropdown();
|
||||
fetch('/service/api/role/switch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: role })
|
||||
}).then(function(r) { return r.json() }).then(function(d) {
|
||||
if (d.ok) { window.location.reload(); }
|
||||
});
|
||||
}
|
||||
|
||||
function resetRole() {
|
||||
closeDropdown();
|
||||
fetch('/service/api/role/reset', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(function(r) { return r.json() }).then(function(d) {
|
||||
if (d.ok) { window.location.reload(); }
|
||||
});
|
||||
}
|
||||
|
||||
// Close dropdown on outside click
|
||||
document.addEventListener('click', function(e) {
|
||||
var sw = document.querySelector('.role-switcher');
|
||||
if (sw && !sw.contains(e.target)) closeDropdown();
|
||||
});
|
||||
|
||||
window.roleSwitcher = { switchRole: switchRole, resetRole: resetRole, ROLE_NAMES: ROLE_NAMES, EMOJIS: EMOJIS };
|
||||
})();
|
||||
@@ -9,15 +9,12 @@
|
||||
function applyTheme(theme) {
|
||||
var resolved = theme === 'system' ? getSystemTheme() : theme;
|
||||
document.documentElement.setAttribute('data-theme', resolved);
|
||||
var label = document.getElementById('theme-label');
|
||||
if (label) {
|
||||
var names = { system: 'Авто', dark: 'Тёмная', light: 'Светлая' };
|
||||
label.textContent = names[theme] || 'Авто';
|
||||
}
|
||||
var names = { system: 'Авто', dark: 'Тёмная', light: 'Светлая' };
|
||||
var icons = { system: '🖥', dark: '🌙', light: '☀️' };
|
||||
var btn = document.getElementById('theme-btn');
|
||||
if (btn) {
|
||||
var icons = { system: '🖥', dark: '🌙', light: '☀️' };
|
||||
btn.textContent = icons[theme] || '🖥';
|
||||
btn.title = names[theme] || 'Авто';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,16 +26,10 @@
|
||||
{% if role_permissions.get('checklist', True) != False %}<a href="/service/checklist" class="{% if active_page == 'checklist' %}active{% endif %}">Чек-лист</a>{% endif %}
|
||||
{% if role_permissions.get('tech_access', True) != False %}<a href="/service/documents/tech-access" class="{% if active_page == 'tech-access' %}active{% endif %}">Доступ техников</a>{% endif %}
|
||||
|
||||
<div class="sidebar-section">Чат-бот Max</div>
|
||||
{% if role_permissions.get('bot_analytics', True) != False %}<a href="/service/bot-settings/analytics" class="{% if active_page == 'bot-analytics' %}active{% endif %}">Аналитика</a>{% endif %}
|
||||
{% if role_permissions.get('bot_users', True) != False %}<a href="/service/bot-settings/users" class="{% if active_page == 'bot-users' %}active{% endif %}">Пользователи бота</a>{% endif %}
|
||||
{% if role_permissions.get('bot_conversations', True) != False %}<a href="/service/bot-settings/conversations" class="{% if active_page == 'bot-conversations' %}active{% endif %}">Диалоги бота</a>{% endif %}
|
||||
{% if role_permissions.get('bot_tickets', True) != False %}<a href="/service/bot-settings/tickets" class="{% if active_page == 'bot-tickets' %}active{% endif %}">Заявки</a>{% endif %}
|
||||
{% if role_permissions.get('bot_handoffs', True) != False %}<a href="/service/bot-settings/handoffs" class="{% if active_page == 'bot-handoffs' %}active{% endif %}">Handoff-диалоги</a>{% endif %}
|
||||
{% if role_permissions.get('bot_notifications', True) != False %}<a href="/service/bot-settings/notifications" class="{% if active_page == 'bot-notifications' %}active{% endif %}">Уведомления</a>{% endif %}
|
||||
{% if role_permissions.get('bot_risk_questions', True) != False %}<a href="/service/bot-settings/risk-questions" class="{% if active_page == 'bot-risk-questions' %}active{% endif %}">Вопросы риск-инжиниринга</a>{% endif %}
|
||||
{% if role_permissions.get('bot_storage', True) != False %}<a href="/service/bot-settings/storage" class="{% if active_page == 'bot-storage' %}active{% endif %}">Яндекс.Диск</a>{% endif %}
|
||||
{% if role_permissions.get('bot_broadcasts', True) != False %}<a href="/service/bot-settings/broadcasts" class="{% if active_page == 'bot-broadcasts' %}active{% endif %}">Рассылки</a>{% endif %}
|
||||
<div class="sidebar-section">Чат-бот MAX</div>
|
||||
{% if role_permissions.get('bot_consent', True) != False %}<a href="/service/bot-consent" class="{% if active_page == 'bot-consent' %}active{% endif %}">Согласия ФЗ-152</a>{% endif %}
|
||||
{% if role_permissions.get('bot_tickets', True) != False %}<a href="/service/bot-tickets" class="{% if active_page == 'bot-tickets' %}active{% endif %}">Заявки от бота</a>{% endif %}
|
||||
{% if role_permissions.get('bot_kb', True) != False %}<a href="/service/bot-kb" class="{% if active_page == 'bot-kb' %}active{% endif %}">База знаний</a>{% endif %}
|
||||
|
||||
<div class="sidebar-section">Контент</div>
|
||||
{% if role_permissions.get('blog', True) != False %}<a href="/service/blog" class="{% if active_page == 'blog' %}active{% endif %}">Управление блогом</a>{% endif %}
|
||||
@@ -48,9 +42,9 @@
|
||||
<div class="sidebar-section">Настройки</div>
|
||||
<a href="/service/questionnaire-config" class="{% if active_page == 'questionnaire-config' %}active{% endif %}">Настройка опросника</a>
|
||||
<a href="/service/formulas" class="{% if active_page == 'formulas' %}active{% endif %}">Формулы</a>
|
||||
<a href="/service/bot-settings" class="{% if active_page == 'bot-settings' %}active{% endif %}">Чат-бот Max</a>
|
||||
<a href="/service/portal-settings" class="{% if active_page == 'portal-settings' %}active{% endif %}">Настройка портала</a>
|
||||
<a href="/service/role-settings" class="{% if active_page == 'role-settings' %}active{% endif %}">Настройка Ролей</a>
|
||||
{% if role_permissions.get('bot_settings', True) != False %}<a href="/service/bot-settings" class="{% if active_page == 'bot-settings' %}active{% endif %}">Настройки чат-бота</a>{% endif %}
|
||||
<a href="/service/ideas" class="{% if active_page == 'ideas' %}active{% endif %}">Идеи</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<script>(function(){var t=localStorage.getItem('aegisone_service_theme')||'system';if(t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme:dark)').matches))document.documentElement.setAttribute('data-theme','dark');else if(t==='light'||(t==='system'&&matchMedia('(prefers-color-scheme:light)').matches))document.documentElement.setAttribute('data-theme','light');})();</script>
|
||||
<title>{% block title %}Service Portal — AegisOne Engineering{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/service/static/css/service.css">
|
||||
<link rel="stylesheet" href="/service/static/css/service-theme.css">
|
||||
@@ -22,15 +23,50 @@
|
||||
<a href="/service/dashboard">AegisOne Service</a>
|
||||
</div>
|
||||
<ul class="navbar__menu">
|
||||
<li class="theme-switcher" data-tooltip="Переключить тему">
|
||||
<button id="theme-btn" onclick="toggleTheme()">🖥</button>
|
||||
<span class="theme-label" id="theme-label">Авто</span>
|
||||
{% if available_roles|length > 1 %}
|
||||
<li class="role-switcher">
|
||||
<button class="role-btn" onclick="document.querySelector('.role-dropdown').classList.toggle('open')">
|
||||
<span class="role-btn-label">{{ role_override_active and '⚡' or '' }} {{ user.role }}</span>
|
||||
<span class="role-btn-arrow">▾</span>
|
||||
</button>
|
||||
<div class="role-dropdown">
|
||||
{% for r in available_roles %}
|
||||
<a href="#" class="role-dropdown-item {% if r == user.role %}active{% endif %}" onclick="event.preventDefault(); roleSwitcher.switchRole('{{ r }}')">
|
||||
{{ r }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if role_override_active %}
|
||||
<div class="role-dropdown-divider"></div>
|
||||
<a href="#" class="role-dropdown-item role-dropdown-reset" onclick="event.preventDefault(); roleSwitcher.resetRole()">
|
||||
← Сбросить ({{ original_role }})
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="theme-switcher">
|
||||
<button id="theme-btn" onclick="toggleTheme()" title="Авто">🖥</button>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
<script src="/service/static/js/theme.js"></script>
|
||||
<script src="/service/static/js/role-switcher.js"></script>
|
||||
<script>
|
||||
function showNotification(msg, type){
|
||||
type = type || 'success';
|
||||
var container = document.getElementById('toast-container');
|
||||
if(!container){ container = document.createElement('div'); container.className='toast-container'; container.id='toast-container'; document.body.appendChild(container); }
|
||||
var t = document.createElement('div');
|
||||
t.className = 'toast toast-' + type;
|
||||
t.textContent = msg;
|
||||
container.appendChild(t);
|
||||
setTimeout(function(){ t.style.opacity='0'; t.style.transition='opacity 0.3s'; setTimeout(function(){ t.remove() },300) }, 3000);
|
||||
}
|
||||
</script>
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,78 +3,7 @@
|
||||
{% block content %}
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">☰</button>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo sidebar-logo-text">Aegis<span>One</span></div>
|
||||
<div style="font-size:12px;color:var(--text-muted);letter-spacing:0.05em;">Service Portal</div>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-section">Панель</div>
|
||||
<a href="/service/dashboard" class="active">Дашборд</a>
|
||||
{% if user.role == 'owner' %}
|
||||
<div class="sidebar-section">Управление</div>
|
||||
<a href="/service/users">Сотрудники</a>
|
||||
<a href="/service/customers">Клиенты</a>
|
||||
<a href="/service/objects">Объекты</a>
|
||||
<a href="/service/assignments">Назначение сотрудников</a>
|
||||
<a href="/service/sla">SLA контракты</a>
|
||||
<a href="/service/ceo">CEO дашборд</a>
|
||||
<a href="/service/coefficients">Коэффициенты</a>
|
||||
<a href="/service/charts">Графики</a>
|
||||
<a href="/service/ideas">Идеи</a>
|
||||
{% elif user.role == 'engineer' %}
|
||||
<div class="sidebar-section">Управление</div>
|
||||
<a href="/service/customers">Клиенты</a>
|
||||
<a href="/service/objects">Объекты</a>
|
||||
<a href="/service/sla">SLA контракты</a>
|
||||
<a href="/service/documents/tech-access">Доступ техников к документам</a>
|
||||
{% endif %}
|
||||
<div class="sidebar-section">Работа</div>
|
||||
{% if user.role in ['engineer', 'technician'] %}
|
||||
<a href="/service/tasks">Задачи</a>
|
||||
<a href="/service/reports">Отчёты</a>
|
||||
<a href="/service/incidents">Инциденты</a>
|
||||
{% endif %}
|
||||
{% if user.role in ['owner', 'engineer'] %}
|
||||
<a href="/service/questionnaire">Опросник</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/questionnaire-config">Настройка опросника</a>
|
||||
{% endif %}
|
||||
{% if user.role in ['owner', 'engineer'] %}
|
||||
<a href="/service/passports">Паспорта объектов</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'technician' %}
|
||||
<a href="/service/checklist">Чек-лист</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'owner' %}
|
||||
<div class="sidebar-section">Внешние системы</div>
|
||||
<a href="https://git.aegisone.ru" target="_blank" rel="noopener">Gitea</a>
|
||||
<a href="{{ PORTAINER_URL }}" target="_blank" rel="noopener">Portainer</a>
|
||||
{% endif %}
|
||||
<div class="sidebar-section">Блог</div>
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/blog">Управление блогом</a>
|
||||
{% endif %}
|
||||
<a href="/blog/">Публичный блог</a>
|
||||
<div class="sidebar-section">Контент</div>
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/cases">Примеры из практики</a>
|
||||
{% endif %}
|
||||
<div class="sidebar-section">Документация</div>
|
||||
{% if user.role in ['owner', 'engineer'] %}
|
||||
<a href="/service/documents/">Просмотр документации</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/documents/admin/permissions">Управление документами</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-name">{{ user.full_name }}</div>
|
||||
<a href="/service/logout" style="display:inline-block;margin:6px 0;font-size:12px;">Выйти</a>
|
||||
<div class="version-pill" onclick="openChangelog()" title="История изменений">v{{ app_version }}</div>
|
||||
</div>
|
||||
</aside>
|
||||
{% include "_sidebar.html" %}
|
||||
<main class="main">
|
||||
<div class="main-header">
|
||||
<div>
|
||||
@@ -182,75 +111,4 @@ document.addEventListener('DOMContentLoaded', applyDashSettings);
|
||||
.card-link { text-decoration:none; color:inherit; display:block; transition:transform 0.2s, box-shadow 0.2s; }
|
||||
.card-link:hover { transform:translateY(-2px); box-shadow:var(--shadow); cursor:pointer; }
|
||||
</style>
|
||||
|
||||
<div class="modal-overlay" id="changelog-modal"><div class="modal">
|
||||
<button type="button" class="modal-close" onclick="closeChangelog()">✕</button>
|
||||
<h2>История изменений</h2>
|
||||
<div id="changelog-content" class="doc-content" style="padding:0;background:transparent;border:none;"></div>
|
||||
</div></div>
|
||||
|
||||
<script>
|
||||
function openChangelog(){
|
||||
var modal = document.getElementById('changelog-modal');
|
||||
modal.classList.add('open');
|
||||
var content = document.getElementById('changelog-content');
|
||||
if(!content.dataset.loaded){
|
||||
content.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
fetch('/service/api/changelog').then(function(r){ return r.json() }).then(function(data){
|
||||
content.innerHTML = data.html || '<p>Нет данных</p>';
|
||||
content.dataset.loaded = '1';
|
||||
});
|
||||
}
|
||||
}
|
||||
function closeChangelog(){
|
||||
document.getElementById('changelog-modal').classList.remove('open');
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var nav = document.querySelector('.navbar__menu');
|
||||
if(!nav) return;
|
||||
var sidebarLinks = document.querySelectorAll('.sidebar-nav a[href]');
|
||||
var saved = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
sidebarLinks.forEach(function(link){
|
||||
var star = document.createElement('span');
|
||||
star.className = 'sidebar-star';
|
||||
var url = link.getAttribute('href');
|
||||
star.textContent = saved.indexOf(url) !== -1 ? '\u2605' : '\u2606';
|
||||
star.style.cssText = 'cursor:pointer;margin-right:6px;font-size:14px;user-select:none;';
|
||||
star.addEventListener('click', function(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
var idx = favs.indexOf(url);
|
||||
if(idx !== -1){ favs.splice(idx,1); star.textContent = '\u2606'; }
|
||||
else { favs.push(url); star.textContent = '\u2605'; }
|
||||
localStorage.setItem('sidebar_favorites', JSON.stringify(favs));
|
||||
renderFavorites();
|
||||
});
|
||||
link.insertBefore(star, link.firstChild);
|
||||
});
|
||||
function renderFavorites(){
|
||||
var existing = nav.querySelectorAll('.nav-fav');
|
||||
existing.forEach(function(e){ e.remove(); });
|
||||
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
favs.forEach(function(url){
|
||||
var orig = document.querySelector('.sidebar-nav a[href="' + url + '"]');
|
||||
if(!orig) return;
|
||||
var li = document.createElement('li');
|
||||
li.className = 'nav-fav';
|
||||
var a = document.createElement('a');
|
||||
a.href = url; a.textContent = orig.textContent.replace(/^[☆★]/, '').trim();
|
||||
li.appendChild(a);
|
||||
nav.insertBefore(li, nav.lastElementChild);
|
||||
});
|
||||
}
|
||||
renderFavorites();
|
||||
// Restore sidebar scroll position
|
||||
var sidebarNav = document.querySelector('.sidebar-nav');
|
||||
var savedScroll = sessionStorage.getItem('sidebar_scroll');
|
||||
if (sidebarNav && savedScroll) sidebarNav.scrollTop = parseInt(savedScroll, 10);
|
||||
// Save scroll position before navigating
|
||||
sidebarNav && sidebarNav.addEventListener('click', function(){
|
||||
sessionStorage.setItem('sidebar_scroll', this.scrollTop);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
<button type="submit" class="btn btn-primary btn-block">Войти</button>
|
||||
</form>
|
||||
<p class="login-box__footnote">* Сервисный портал для сотрудников AegisOne Engineering</p>
|
||||
<div style="text-align:right;margin-top:12px;font-size:11px;color:var(--text-muted);">v{{ app_version }}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,78 +3,7 @@
|
||||
{% block content %}
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">☰</button>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo sidebar-logo-text">Aegis<span>One</span></div>
|
||||
<div style="font-size:12px;color:var(--text-muted);letter-spacing:0.05em;">Service Portal</div>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-section">Панель</div>
|
||||
<a href="/service/dashboard" class="{% if active_page == 'dashboard' %}active{% endif %}">Дашборд</a>
|
||||
{% if user.role == 'owner' %}
|
||||
<div class="sidebar-section">Управление</div>
|
||||
<a href="/service/users" class="{% if active_page == 'users' %}active{% endif %}">Сотрудники</a>
|
||||
<a href="/service/customers" class="{% if active_page == 'customers' %}active{% endif %}">Клиенты</a>
|
||||
<a href="/service/objects" class="{% if active_page == 'objects' %}active{% endif %}">Объекты</a>
|
||||
<a href="/service/assignments" class="{% if active_page == 'assignments' %}active{% endif %}">Назначение сотрудников</a>
|
||||
<a href="/service/sla" class="{% if active_page == 'sla' %}active{% endif %}">SLA контракты</a>
|
||||
<a href="/service/ceo" class="{% if active_page == 'ceo' %}active{% endif %}">CEO дашборд</a>
|
||||
<a href="/service/coefficients" class="{% if active_page == 'coefficients' %}active{% endif %}">Коэффициенты</a>
|
||||
<a href="/service/coefficients/test" class="{% if active_page == 'coefficients-test' %}active{% endif %}">Тест формул</a>
|
||||
<a href="/service/charts" class="{% if active_page == 'charts' %}active{% endif %}">Графики</a>
|
||||
<a href="/service/ideas" class="{% if active_page == 'ideas' %}active{% endif %}">Идеи</a>
|
||||
{% elif user.role == 'engineer' %}
|
||||
<div class="sidebar-section">Управление</div>
|
||||
<a href="/service/customers" class="{% if active_page == 'customers' %}active{% endif %}">Клиенты</a>
|
||||
<a href="/service/objects" class="{% if active_page == 'objects' %}active{% endif %}">Объекты</a>
|
||||
<a href="/service/sla" class="{% if active_page == 'sla' %}active{% endif %}">SLA контракты</a>
|
||||
<a href="/service/documents/tech-access" class="{% if active_page == 'tech-access' %}active{% endif %}">Доступ техников к документам</a>
|
||||
{% endif %}
|
||||
<div class="sidebar-section">Работа</div>
|
||||
{% if user.role in ['engineer', 'technician'] %}
|
||||
<a href="/service/tasks" class="{% if active_page == 'tasks' %}active{% endif %}">Задачи</a>
|
||||
<a href="/service/reports" class="{% if active_page == 'reports' %}active{% endif %}">Отчёты</a>
|
||||
<a href="/service/incidents" class="{% if active_page == 'incidents' %}active{% endif %}">Инциденты</a>
|
||||
{% endif %}
|
||||
{% if user.role in ['owner', 'engineer'] %}
|
||||
<a href="/service/questionnaire" class="{% if active_page == 'questionnaire' %}active{% endif %}">Опросник</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/questionnaire-config" class="{% if active_page == 'questionnaire-config' %}active{% endif %}">Настройка опросника</a>
|
||||
{% endif %}
|
||||
{% if user.role in ['owner', 'engineer'] %}
|
||||
<a href="/service/passports" class="{% if active_page == 'passports' %}active{% endif %}">Паспорта объектов</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'technician' %}
|
||||
<a href="/service/checklist" class="{% if active_page == 'checklist' %}active{% endif %}">Чек-лист</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'owner' %}
|
||||
<div class="sidebar-section">Внешние системы</div>
|
||||
<a href="https://git.aegisone.ru" target="_blank" rel="noopener">Gitea</a>
|
||||
<a href="{{ PORTAINER_URL }}" target="_blank" rel="noopener">Portainer</a>
|
||||
{% endif %}
|
||||
<div class="sidebar-section">Блог</div>
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/blog" class="{% if active_page == 'blog' %}active{% endif %}">Управление блогом</a>
|
||||
{% endif %}
|
||||
<div class="sidebar-section">Контент</div>
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/cases" class="{% if active_page == 'cases' %}active{% endif %}">Примеры из практики</a>
|
||||
{% endif %}
|
||||
<div class="sidebar-section">Документация</div>
|
||||
{% if user.role in ['owner', 'engineer'] %}
|
||||
<a href="/service/documents/" class="{% if active_page == 'docs' %}active{% endif %}">Просмотр документации</a>
|
||||
{% endif %}
|
||||
{% if user.role == 'owner' %}
|
||||
<a href="/service/documents/admin/permissions" class="{% if active_page == 'docs-manage' %}active{% endif %}">Управление документами</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-name">{{ user.full_name }}</div>
|
||||
<a href="/service/logout" style="display:inline-block;margin:6px 0;font-size:12px;">Выйти</a>
|
||||
<div class="version-pill" onclick="openChangelog()" title="История изменений">v{{ app_version }}</div>
|
||||
</div>
|
||||
</aside>
|
||||
{% include "_sidebar.html" %}
|
||||
<main class="main">
|
||||
<div class="main-header">
|
||||
<div>
|
||||
@@ -91,75 +20,5 @@
|
||||
{% block page_content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
<div class="modal-overlay" id="changelog-modal"><div class="modal">
|
||||
<button type="button" class="modal-close" onclick="closeChangelog()">✕</button>
|
||||
<h2>История изменений</h2>
|
||||
<div id="changelog-content" class="doc-content" style="padding:0;background:transparent;border:none;"></div>
|
||||
</div></div>
|
||||
|
||||
<script>
|
||||
function openChangelog(){
|
||||
var modal = document.getElementById('changelog-modal');
|
||||
modal.classList.add('open');
|
||||
var content = document.getElementById('changelog-content');
|
||||
if(!content.dataset.loaded){
|
||||
content.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
fetch('/service/api/changelog').then(function(r){ return r.json() }).then(function(data){
|
||||
content.innerHTML = data.html || '<p>Нет данных</p>';
|
||||
content.dataset.loaded = '1';
|
||||
});
|
||||
}
|
||||
}
|
||||
function closeChangelog(){
|
||||
document.getElementById('changelog-modal').classList.remove('open');
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var nav = document.querySelector('.navbar__menu');
|
||||
if(!nav) return;
|
||||
var sidebarLinks = document.querySelectorAll('.sidebar-nav a[href]');
|
||||
var saved = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
sidebarLinks.forEach(function(link){
|
||||
var star = document.createElement('span');
|
||||
star.className = 'sidebar-star';
|
||||
var url = link.getAttribute('href');
|
||||
star.textContent = saved.indexOf(url) !== -1 ? '\u2605' : '\u2606';
|
||||
star.style.cssText = 'cursor:pointer;margin-right:6px;font-size:14px;user-select:none;';
|
||||
star.addEventListener('click', function(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
var idx = favs.indexOf(url);
|
||||
if(idx !== -1){ favs.splice(idx,1); star.textContent = '\u2606'; }
|
||||
else { favs.push(url); star.textContent = '\u2605'; }
|
||||
localStorage.setItem('sidebar_favorites', JSON.stringify(favs));
|
||||
renderFavorites();
|
||||
});
|
||||
link.insertBefore(star, link.firstChild);
|
||||
});
|
||||
function renderFavorites(){
|
||||
var existing = nav.querySelectorAll('.nav-fav');
|
||||
existing.forEach(function(e){ e.remove(); });
|
||||
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
favs.forEach(function(url){
|
||||
var orig = document.querySelector('.sidebar-nav a[href="' + url + '"]');
|
||||
if(!orig) return;
|
||||
var li = document.createElement('li');
|
||||
li.className = 'nav-fav';
|
||||
var a = document.createElement('a');
|
||||
a.href = url; a.textContent = orig.textContent.replace(/^[☆★]/, '').trim();
|
||||
li.appendChild(a);
|
||||
nav.insertBefore(li, nav.lastElementChild);
|
||||
});
|
||||
}
|
||||
renderFavorites();
|
||||
// Restore sidebar scroll position
|
||||
var sidebarNav = document.querySelector('.sidebar-nav');
|
||||
var savedScroll = sessionStorage.getItem('sidebar_scroll');
|
||||
if (sidebarNav && savedScroll) sidebarNav.scrollTop = parseInt(savedScroll, 10);
|
||||
// Save scroll position before navigating
|
||||
sidebarNav && sidebarNav.addEventListener('click', function(){
|
||||
sessionStorage.setItem('sidebar_scroll', this.scrollTop);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.broadcast-layout { display:flex; gap:24px; margin-top:16px; flex-wrap:wrap; }
|
||||
.broadcast-left { flex:1; min-width:300px; }
|
||||
.broadcast-right { flex:1; min-width:300px; }
|
||||
.msg-preview { background:#e3f2fd; border-radius:12px; padding:12px 16px; margin-top:8px;
|
||||
white-space:pre-wrap; word-break:break-word; line-height:1.5; font-size:14px; }
|
||||
.msg-preview.empty { color:#999; font-style:italic; }
|
||||
.recipient-item { display:flex; align-items:center; gap:8px; padding:6px 8px;
|
||||
border-bottom:1px solid #eee; cursor:pointer; }
|
||||
.recipient-item:hover { background:#f8f9fa; }
|
||||
.recipient-item .name { flex:1; }
|
||||
.recipient-item .phone { color:#666; font-size:13px; }
|
||||
.recipient-list { max-height:300px; overflow-y:auto; border:1px solid #ddd; border-radius:8px;
|
||||
margin-top:8px; }
|
||||
.recipient-list .select-all { padding:8px; border-bottom:1px solid #eee; background:#f8f9fa;
|
||||
font-weight:600; cursor:pointer; user-select:none; }
|
||||
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0;
|
||||
background:rgba(0,0,0,0.5); z-index:1000;
|
||||
align-items:center; justify-content:center; }
|
||||
.modal-overlay.active { display:flex; }
|
||||
.modal-box { background:#fff; border-radius:12px; padding:24px; max-width:600px;
|
||||
width:90%; max-height:80vh; overflow-y:auto; }
|
||||
.modal-box h3 { margin:0 0 16px; }
|
||||
.modal-box .actions { display:flex; gap:12px; justify-content:flex-end; margin-top:20px; }
|
||||
.broadcast-history { margin-top:24px; }
|
||||
.broadcast-history table { width:100%; }
|
||||
.broadcast-history td, .broadcast-history th { padding:8px 12px; text-align:left; border-bottom:1px solid #eee; }
|
||||
.broadcast-history .text-col { max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Рассылки</h2></div>
|
||||
<div class="broadcast-layout">
|
||||
<div class="broadcast-left">
|
||||
<div class="form-group">
|
||||
<label>Текст сообщения</label>
|
||||
<textarea id="broadcast-text" rows="5" style="width:100%;" oninput="updatePreview()" placeholder="Введите текст рассылки... Markdown поддерживается"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>📱 Предпросмотр сообщения</label>
|
||||
<div id="live-preview" class="msg-preview empty">Текст сообщения появится здесь...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="broadcast-right">
|
||||
<div class="form-group">
|
||||
<label>Получатели</label>
|
||||
<input type="text" id="recipient-search" placeholder="Поиск..." style="width:100%;margin-bottom:8px;" oninput="loadRecipients()">
|
||||
</div>
|
||||
<div class="recipient-list" id="recipient-list">
|
||||
<div class="select-all" onclick="toggleSelectAll()">
|
||||
<span id="select-all-checkbox">☐</span> Выбрать всех согласных
|
||||
<span style="float:right;color:#666;font-weight:400;" id="recipient-count"></span>
|
||||
</div>
|
||||
<div id="recipient-items"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;margin-top:12px;">
|
||||
<button class="btn btn-primary" onclick="previewBroadcast()">👁 Предпросмотр</button>
|
||||
<button class="btn btn-primary" onclick="sendBroadcast()">📨 Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card broadcast-history">
|
||||
<div class="card-header"><h3>История рассылок</h3></div>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>ID</th><th>Текст</th><th>Получателей</th><th>Дата</th></tr></thead>
|
||||
<tbody id="history-list"></tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="preview-modal">
|
||||
<div class="modal-box">
|
||||
<h3>👁 Предпросмотр рассылки</h3>
|
||||
<div style="background:#e3f2fd;border-radius:12px;padding:16px;white-space:pre-wrap;line-height:1.5;" id="preview-text"></div>
|
||||
<div style="margin-top:16px;padding:12px;background:#f8f9fa;border-radius:8px;">
|
||||
<strong>Получателей:</strong> <span id="preview-count"></span>
|
||||
<div style="margin-top:8px;max-height:150px;overflow-y:auto;" id="preview-recipients"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="closePreview()">Отмена</button>
|
||||
<button class="btn btn-primary" id="preview-send-btn" onclick="confirmSend()">📨 Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var selectedUserIds = new Set();
|
||||
var allRecipients = [];
|
||||
var currentDraftId = null;
|
||||
|
||||
function updatePreview(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
var preview = document.getElementById('live-preview');
|
||||
if(text){
|
||||
preview.textContent = text;
|
||||
preview.className = 'msg-preview';
|
||||
} else {
|
||||
preview.textContent = 'Текст сообщения появится здесь...';
|
||||
preview.className = 'msg-preview empty';
|
||||
}
|
||||
}
|
||||
|
||||
function loadRecipients(){
|
||||
var q = document.getElementById('recipient-search').value.trim();
|
||||
var url = '/service/api/bot/broadcast/recipients?limit=200';
|
||||
if(q) url += '&q=' + encodeURIComponent(q);
|
||||
fetch(url).then(function(r){ return r.json() }).then(function(d){
|
||||
allRecipients = d.users || [];
|
||||
renderRecipients();
|
||||
});
|
||||
}
|
||||
|
||||
function renderRecipients(){
|
||||
var container = document.getElementById('recipient-items');
|
||||
var countEl = document.getElementById('recipient-count');
|
||||
container.innerHTML = '';
|
||||
countEl.textContent = allRecipients.length + ' чел.';
|
||||
allRecipients.forEach(function(u){
|
||||
var div = document.createElement('div');
|
||||
div.className = 'recipient-item';
|
||||
var checked = selectedUserIds.has(u.max_user_id) ? '☑' : '☐';
|
||||
var name = (u.first_name || '') + ' ' + (u.last_name || '');
|
||||
if(!name.trim()) name = 'ID ' + u.max_user_id;
|
||||
div.innerHTML = '<span class="cb">' + checked + '</span>' +
|
||||
'<span class="name">' + name + '</span>' +
|
||||
'<span class="phone">' + (u.phone || '') + '</span>';
|
||||
div.onclick = function(){ toggleRecipient(u.max_user_id); };
|
||||
container.appendChild(div);
|
||||
});
|
||||
updateSelectAllCheckbox();
|
||||
}
|
||||
|
||||
function toggleRecipient(id){
|
||||
if(selectedUserIds.has(id)){
|
||||
selectedUserIds['delete'](id);
|
||||
} else {
|
||||
selectedUserIds.add(id);
|
||||
}
|
||||
renderRecipients();
|
||||
}
|
||||
|
||||
function toggleSelectAll(){
|
||||
if(selectedUserIds.size === allRecipients.length){
|
||||
selectedUserIds.clear();
|
||||
} else {
|
||||
selectedUserIds = new Set(allRecipients.map(function(u){ return u.max_user_id; }));
|
||||
}
|
||||
renderRecipients();
|
||||
}
|
||||
|
||||
function updateSelectAllCheckbox(){
|
||||
var cb = document.getElementById('select-all-checkbox');
|
||||
if(allRecipients.length > 0 && selectedUserIds.size === allRecipients.length){
|
||||
cb.textContent = '☑';
|
||||
} else {
|
||||
cb.textContent = '☐';
|
||||
}
|
||||
}
|
||||
|
||||
function previewBroadcast(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
if(!text){ alert('Введите текст рассылки'); return; }
|
||||
var userIds = Array.from(selectedUserIds);
|
||||
fetch('/service/api/bot/broadcast', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({text: text, user_ids: userIds, action: 'preview', draft_id: currentDraftId})
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.error){ alert(d.error); return; }
|
||||
currentDraftId = d.draft_id;
|
||||
document.getElementById('preview-text').textContent = d.text;
|
||||
document.getElementById('preview-count').textContent = d.recipient_count + ' чел.';
|
||||
var recEl = document.getElementById('preview-recipients');
|
||||
recEl.innerHTML = d.recipients.join(', ');
|
||||
document.getElementById('preview-modal').className = 'modal-overlay active';
|
||||
});
|
||||
}
|
||||
|
||||
function closePreview(){
|
||||
document.getElementById('preview-modal').className = 'modal-overlay';
|
||||
}
|
||||
|
||||
function confirmSend(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
if(!currentDraftId){ alert('Сначала сделайте предпросмотр'); return; }
|
||||
if(!confirm('Отправить рассылку?')) return;
|
||||
var userIds = Array.from(selectedUserIds);
|
||||
document.getElementById('preview-send-btn').disabled = true;
|
||||
fetch('/service/api/bot/broadcast', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({text: text, user_ids: userIds, action: 'send', draft_id: currentDraftId})
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
closePreview();
|
||||
if(d.ok){
|
||||
alert('Рассылка отправлена: ' + d.sent_to + ' из ' + d.total_recipients + ' получателей');
|
||||
document.getElementById('broadcast-text').value = '';
|
||||
document.getElementById('live-preview').textContent = 'Текст сообщения появится здесь...';
|
||||
document.getElementById('live-preview').className = 'msg-preview empty';
|
||||
selectedUserIds.clear();
|
||||
renderRecipients();
|
||||
currentDraftId = null;
|
||||
loadHistory();
|
||||
} else {
|
||||
alert('Ошибка: ' + (d.error || 'неизвестная'));
|
||||
}
|
||||
document.getElementById('preview-send-btn').disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function sendBroadcast(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
if(!text){ alert('Введите текст рассылки'); return; }
|
||||
var userIds = Array.from(selectedUserIds);
|
||||
if(userIds.length === 0 && !confirm('Не выбрано ни одного получателя. Отправить всем согласным?')) return;
|
||||
previewBroadcast();
|
||||
}
|
||||
|
||||
function loadHistory(){
|
||||
fetch('/service/api/bot/broadcast/history').then(function(r){ return r.json() }).then(function(d){
|
||||
var tbody = document.getElementById('history-list');
|
||||
tbody.innerHTML = '';
|
||||
(d.broadcasts || []).forEach(function(b){
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td>' + b.id + '</td>' +
|
||||
'<td class="text-col" title="' + b.text.replace(/"/g,'"') + '">' + b.text + '</td>' +
|
||||
'<td>' + b.recipient_count + '</td>' +
|
||||
'<td>' + b.sent_at + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
if(!d.broadcasts || d.broadcasts.length === 0){
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#999;">Рассылок ещё не было</td></tr>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadRecipients();
|
||||
loadHistory();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,220 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.consent-table { width:100%; border-collapse:collapse; }
|
||||
.consent-table th, .consent-table td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; }
|
||||
.consent-table th { font-weight:600; color:var(--text-muted); background:var(--light-bg); }
|
||||
.consent-table td { vertical-align:middle; }
|
||||
.consent-yes { color:#155724; font-weight:600; }
|
||||
.consent-no { color:#721c24; font-weight:600; }
|
||||
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; flex-wrap:wrap; }
|
||||
.filter-bar input, .filter-bar select { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
|
||||
.broadcast-card { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:20px; margin-top:16px; }
|
||||
.broadcast-card h3 { margin-bottom:12px; }
|
||||
.broadcast-card textarea { width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:14px; min-height:100px; resize:vertical; margin-bottom:10px; box-sizing:border-box; }
|
||||
.broadcast-actions { display:flex; gap:10px; flex-wrap:wrap; }
|
||||
.recipient-count { font-size:12px; color:var(--text-muted); margin-bottom:8px; }
|
||||
|
||||
.preview-area { display:none; margin-top:12px; padding:12px; border:1px solid var(--border); border-radius:8px; background:var(--bg-input); white-space:pre-wrap; font-size:14px; line-height:1.5; }
|
||||
.preview-area.open { display:block; }
|
||||
.preview-area .preview-text { margin-bottom:10px; }
|
||||
.preview-area .preview-actions { display:flex; gap:10px; }
|
||||
|
||||
.confirm-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
|
||||
.confirm-overlay.open { display:flex; }
|
||||
.confirm-modal { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:24px; width:500px; max-width:90vw; }
|
||||
.confirm-modal h3 { margin-bottom:8px; }
|
||||
.confirm-modal .confirm-text { padding:10px; border:1px solid var(--border); border-radius:6px; background:var(--bg-input); max-height:200px; overflow-y:auto; margin:12px 0; white-space:pre-wrap; font-size:13px; }
|
||||
.confirm-modal .confirm-count { font-size:12px; color:var(--text-muted); margin-bottom:12px; }
|
||||
.confirm-modal .confirm-timer { font-size:24px; font-weight:700; text-align:center; color:var(--accent); margin:12px 0; }
|
||||
.confirm-modal .confirm-actions { display:flex; gap:10px; justify-content:center; }
|
||||
.confirm-modal .confirm-actions .btn { min-width:140px; }
|
||||
.timer-danger { color:var(--danger) !important; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h2>Согласия ФЗ-152</h2>
|
||||
<span id="consent-count" style="font-size:13px;color:var(--text-muted);">0 пользователей</span>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<input type="text" id="consent-search" placeholder="Поиск по имени, телефону..." oninput="filterConsents()">
|
||||
</div>
|
||||
|
||||
<table class="consent-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Имя</th>
|
||||
<th>Телефон</th>
|
||||
<th>Email</th>
|
||||
<th>Организация</th>
|
||||
<th>Username</th>
|
||||
<th>Согласие</th>
|
||||
<th>Дата</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="consent-table-body">
|
||||
<tr><td colspan="8" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="broadcast-card">
|
||||
<h3>Рассылка пользователям MAX</h3>
|
||||
<div class="recipient-count" id="recipient-count">Получателей: 0</div>
|
||||
<textarea id="broadcast-text" placeholder="Введите текст сообщения..." oninput="clearPreview()"></textarea>
|
||||
<div class="broadcast-actions">
|
||||
<button class="btn" onclick="showPreview()">Предпросмотр</button>
|
||||
<button class="btn btn-primary" id="send-btn" onclick="openConfirm()" disabled>Отправить всем</button>
|
||||
</div>
|
||||
<div class="preview-area" id="preview-area">
|
||||
<div class="preview-text" id="preview-text"></div>
|
||||
<div class="preview-actions">
|
||||
<button class="btn btn-primary" onclick="openConfirm()">Отправить</button>
|
||||
<button class="btn" onclick="hidePreview()">Редактировать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-overlay" id="confirm-overlay">
|
||||
<div class="confirm-modal">
|
||||
<h3>Подтверждение рассылки</h3>
|
||||
<div class="confirm-count" id="confirm-count">Получателей: 0</div>
|
||||
<div class="confirm-text" id="confirm-text"></div>
|
||||
<div class="confirm-timer" id="confirm-timer">10</div>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn btn-primary" id="confirm-yes" onclick="executeBroadcast()">Подтвердить</button>
|
||||
<button class="btn" onclick="closeConfirm()">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var allConsents = [];
|
||||
var confirmTimer = null;
|
||||
var confirmSeconds = 10;
|
||||
|
||||
function loadConsents(){
|
||||
var tbody = document.getElementById('consent-table-body');
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>';
|
||||
fetch('/api/bot/users/consented').then(function(r){ return r.json() }).then(function(data){
|
||||
allConsents = data;
|
||||
document.getElementById('consent-count').textContent = data.length + ' пользователей';
|
||||
document.getElementById('recipient-count').textContent = 'Получателей: ' + data.length;
|
||||
document.getElementById('send-btn').disabled = data.length === 0;
|
||||
renderConsents(data);
|
||||
}).catch(function(err){
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:30px;color:var(--text-muted);">Ошибка загрузки</td></tr>';
|
||||
showNotification('Ошибка загрузки согласий', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function renderConsents(list){
|
||||
var tbody = document.getElementById('consent-table-body');
|
||||
if(!list.length){
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:30px;color:var(--text-muted);">Нет данных</td></tr>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
list.forEach(function(u){
|
||||
var name = [u.first_name, u.last_name].filter(Boolean).join(' ') || '—';
|
||||
var dateStr = u.consent_date ? new Date(u.consent_date).toLocaleString() : '—';
|
||||
html += '<tr>' +
|
||||
'<td>' + u.id + '</td>' +
|
||||
'<td>' + escapeHtml(name) + '</td>' +
|
||||
'<td>' + escapeHtml(u.phone || '—') + '</td>' +
|
||||
'<td>' + escapeHtml(u.email || '—') + '</td>' +
|
||||
'<td>' + escapeHtml(u.organization || '—') + '</td>' +
|
||||
'<td>' + escapeHtml(u.username || '—') + '</td>' +
|
||||
'<td class="consent-yes">Да</td>' +
|
||||
'<td style="font-size:12px;color:var(--text-muted);">' + dateStr + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function filterConsents(){
|
||||
var search = document.getElementById('consent-search').value.toLowerCase();
|
||||
var filtered = allConsents.filter(function(u){
|
||||
if(!search) return true;
|
||||
var name = [u.first_name, u.last_name].filter(Boolean).join(' ').toLowerCase();
|
||||
var phone = (u.phone || '').toLowerCase();
|
||||
var email = (u.email || '').toLowerCase();
|
||||
var username = (u.username || '').toLowerCase();
|
||||
return name.includes(search) || phone.includes(search) || email.includes(search) || username.includes(search);
|
||||
});
|
||||
renderConsents(filtered);
|
||||
}
|
||||
|
||||
function clearPreview(){
|
||||
document.getElementById('preview-area').classList.remove('open');
|
||||
}
|
||||
|
||||
function showPreview(){
|
||||
var text = document.getElementById('broadcast-text').value;
|
||||
if(!text.trim()){ showNotification('Введите текст сообщения', 'warning'); return; }
|
||||
document.getElementById('preview-text').textContent = text;
|
||||
document.getElementById('preview-area').classList.add('open');
|
||||
}
|
||||
|
||||
function hidePreview(){
|
||||
document.getElementById('preview-area').classList.remove('open');
|
||||
}
|
||||
|
||||
function openConfirm(){
|
||||
if(confirmTimer) return;
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
if(!text){ showNotification('Введите текст сообщения', 'warning'); return; }
|
||||
closeConfirm();
|
||||
document.getElementById('confirm-text').textContent = text;
|
||||
document.getElementById('confirm-count').textContent = 'Получателей: ' + allConsents.length;
|
||||
document.getElementById('confirm-overlay').classList.add('open');
|
||||
confirmSeconds = 10;
|
||||
document.getElementById('confirm-timer').textContent = confirmSeconds;
|
||||
document.getElementById('confirm-timer').className = 'confirm-timer';
|
||||
document.getElementById('confirm-yes').disabled = false;
|
||||
confirmTimer = setInterval(function(){
|
||||
confirmSeconds--;
|
||||
document.getElementById('confirm-timer').textContent = confirmSeconds;
|
||||
if(confirmSeconds <= 3) document.getElementById('confirm-timer').className = 'confirm-timer timer-danger';
|
||||
if(confirmSeconds <= 0){
|
||||
clearInterval(confirmTimer);
|
||||
confirmTimer = null;
|
||||
closeConfirm();
|
||||
showNotification('Время подтверждения истекло. Нажмите «Отправить всем» повторно.', 'warning');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function closeConfirm(){
|
||||
if(confirmTimer){ clearInterval(confirmTimer); confirmTimer = null; }
|
||||
document.getElementById('confirm-overlay').classList.remove('open');
|
||||
}
|
||||
|
||||
function executeBroadcast(){
|
||||
if(confirmTimer){ clearInterval(confirmTimer); confirmTimer = null; }
|
||||
var text = document.getElementById('confirm-text').textContent;
|
||||
document.getElementById('confirm-yes').disabled = true;
|
||||
fetch('/api/bot/broadcast', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({text: text, all: true})
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
closeConfirm();
|
||||
if(d.ok) showNotification('Отправлено: ' + d.sent + ', ошибок: ' + d.errors, d.errors ? 'warning' : 'success');
|
||||
else showNotification('Ошибка: ' + (d.error || 'неизвестная'), 'error');
|
||||
}).catch(function(){
|
||||
closeConfirm();
|
||||
showNotification('Ошибка соединения', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(s){
|
||||
if(!s) return '';
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
loadConsents();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,239 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.kb-table { width:100%; border-collapse:collapse; }
|
||||
.kb-table th, .kb-table td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; }
|
||||
.kb-table th { font-weight:600; color:var(--text-muted); background:var(--light-bg); }
|
||||
.kb-table td { vertical-align:top; }
|
||||
.kb-table .active-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:600; }
|
||||
.kb-table .active-badge.yes { background:#d4edda; color:#155724; }
|
||||
.kb-table .active-badge.no { background:#f8d7da; color:#721c24; }
|
||||
.kb-modal { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.4);
|
||||
z-index:1000; align-items:center; justify-content:center; }
|
||||
.kb-modal.open { display:flex; }
|
||||
.kb-modal-content { background:var(--bg-card); border-radius:12px; padding:30px; width:600px; max-width:90vw;
|
||||
max-height:80vh; overflow-y:auto; }
|
||||
.kb-modal-content h3 { margin-bottom:20px; }
|
||||
.form-group { margin-bottom:14px; }
|
||||
.form-group label { display:block; font-weight:600; margin-bottom:4px; font-size:13px; color:var(--text-muted); }
|
||||
.form-group input, .form-group textarea, .form-group select {
|
||||
width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:14px;
|
||||
}
|
||||
.form-group textarea { min-height:100px; resize:vertical; }
|
||||
.modal-actions { display:flex; gap:12px; margin-top:16px; }
|
||||
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; flex-wrap:wrap; }
|
||||
.filter-bar input, .filter-bar select { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h2>База знаний чат-бота</h2>
|
||||
<button class="btn btn-primary" onclick="openModal()">+ Добавить карточку</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<input type="text" id="kb-search" placeholder="Поиск по вопросу..." oninput="filterKB()">
|
||||
<select id="kb-category-filter" onchange="filterKB()">
|
||||
<option value="">Все категории</option>
|
||||
</select>
|
||||
<select id="kb-active-filter" onchange="filterKB()">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="true">Активные</option>
|
||||
<option value="false">Неактивные</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<table class="kb-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Вопрос</th>
|
||||
<th>Категория</th>
|
||||
<th>Статус</th>
|
||||
<th>Источник</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="kb-table-body">
|
||||
<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="kb-modal" id="kb-modal">
|
||||
<div class="kb-modal-content">
|
||||
<h3 id="modal-title">Новая карточка</h3>
|
||||
<form id="kb-form">
|
||||
<div class="form-group">
|
||||
<label>Категория</label>
|
||||
<select name="category_id" id="kb-category-id"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Вопрос *</label>
|
||||
<input type="text" name="question" id="kb-question" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ответ (оставьте пустым, если нет ответа)</label>
|
||||
<textarea name="answer" id="kb-answer"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ключевые слова (через запятую)</label>
|
||||
<input type="text" name="keywords" id="kb-keywords" placeholder="слово1, слово2, слово3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>URL источника</label>
|
||||
<input type="url" name="source_url" id="kb-source-url" placeholder="https://aegisone.ru/...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" name="is_active" id="kb-is-active" checked> Активная карточка</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var categories = {};
|
||||
var allCards = [];
|
||||
|
||||
fetch('/service/api/bot/settings').then(function(r){ return r.json() }).then(function(d){});
|
||||
|
||||
function loadKB(){
|
||||
fetch('/api/bot/kb').then(function(r){ return r.json() }).then(function(data){
|
||||
allCards = data;
|
||||
renderKB(data);
|
||||
});
|
||||
}
|
||||
|
||||
function renderKB(cards){
|
||||
var tbody = document.getElementById('kb-table-body');
|
||||
if(!cards.length){
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Нет карточек</td></tr>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
cards.forEach(function(c){
|
||||
var catName = categories[c.category_id] || 'Без категории';
|
||||
var activeClass = c.is_active ? 'yes' : 'no';
|
||||
var activeLabel = c.is_active ? 'Активна' : 'Неактивна';
|
||||
var answerPreview = c.answer ? c.answer.substring(0, 50) + '...' : '<em style="color:var(--text-muted)">Нет ответа</em>';
|
||||
html += '<tr>' +
|
||||
'<td>' + c.id + '</td>' +
|
||||
'<td><strong>' + escapeHtml(c.question) + '</strong><br><small style="color:var(--text-muted)">' + answerPreview + '</small></td>' +
|
||||
'<td>' + catName + '</td>' +
|
||||
'<td><span class="active-badge ' + activeClass + '">' + activeLabel + '</span></td>' +
|
||||
'<td style="font-size:12px;">' + (c.source_url ? '<a href="' + c.source_url + '" target="_blank">ссылка</a>' : '—') + '</td>' +
|
||||
'<td><button class="btn btn-sm" onclick="editCard(' + c.id + ')" title="Редактировать">✏️</button> <button class="btn btn-sm" onclick="deleteCard(' + c.id + ')" style="color:#dc3545;" title="Удалить">🗑</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function filterKB(){
|
||||
var search = document.getElementById('kb-search').value.toLowerCase();
|
||||
var catFilter = document.getElementById('kb-category-filter').value;
|
||||
var activeFilter = document.getElementById('kb-active-filter').value;
|
||||
var filtered = allCards.filter(function(c){
|
||||
if(search && !c.question.toLowerCase().includes(search)) return false;
|
||||
if(catFilter && String(c.category_id) !== catFilter) return false;
|
||||
if(activeFilter !== '' && String(c.is_active) !== activeFilter) return false;
|
||||
return true;
|
||||
});
|
||||
renderKB(filtered);
|
||||
}
|
||||
|
||||
function openModal(){
|
||||
editingId = null;
|
||||
document.getElementById('kb-modal').classList.add('open');
|
||||
document.getElementById('modal-title').textContent = 'Новая карточка';
|
||||
document.getElementById('kb-form').reset();
|
||||
document.getElementById('kb-is-active').checked = true;
|
||||
}
|
||||
|
||||
function closeModal(){
|
||||
document.getElementById('kb-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
var editingId = null;
|
||||
|
||||
function editCard(id){
|
||||
var card = allCards.find(function(c){ return c.id === id; });
|
||||
if(!card) return;
|
||||
editingId = id;
|
||||
document.getElementById('modal-title').textContent = 'Редактировать карточку #' + id;
|
||||
document.getElementById('kb-question').value = card.question || '';
|
||||
document.getElementById('kb-answer').value = card.answer || '';
|
||||
document.getElementById('kb-keywords').value = (card.keywords || []).join(', ');
|
||||
document.getElementById('kb-source-url').value = card.source_url || '';
|
||||
document.getElementById('kb-is-active').checked = card.is_active;
|
||||
document.getElementById('kb-category-id').value = card.category_id || '';
|
||||
document.getElementById('kb-modal').classList.add('open');
|
||||
}
|
||||
|
||||
function deleteCard(id){
|
||||
if(!confirm('Удалить карточку?')) return;
|
||||
fetch('/api/bot/kb/' + id, { method: 'DELETE' }).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.ok) { showNotification('Карточка удалена', 'success'); loadKB(); }
|
||||
else showNotification('Ошибка удаления', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('kb-form').addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
var data = {
|
||||
category_id: parseInt(document.getElementById('kb-category-id').value) || null,
|
||||
question: document.getElementById('kb-question').value,
|
||||
answer: document.getElementById('kb-answer').value || null,
|
||||
keywords: document.getElementById('kb-keywords').value.split(',').map(function(s){ return s.trim(); }).filter(Boolean),
|
||||
source_url: document.getElementById('kb-source-url').value || null,
|
||||
is_active: document.getElementById('kb-is-active').checked,
|
||||
sort_order: 0,
|
||||
};
|
||||
var url = '/api/bot/kb';
|
||||
var method = 'POST';
|
||||
if(editingId) { url += '/' + editingId; method = 'PUT'; }
|
||||
fetch(url, {
|
||||
method: method, headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.ok) { closeModal(); loadKB(); showNotification(editingId ? 'Карточка обновлена' : 'Карточка создана', 'success'); }
|
||||
else showNotification('Ошибка сохранения', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
function escapeHtml(s){
|
||||
if(!s) return '';
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// Load categories
|
||||
fetch('/api/bot/kb').then(function(r){ return r.json() }).then(function(data){
|
||||
// Extract unique categories
|
||||
var catSet = {};
|
||||
data.forEach(function(c){
|
||||
if(c.category_id) catSet[c.category_id] = true;
|
||||
});
|
||||
var catSelect = document.getElementById('kb-category-id');
|
||||
var catFilter = document.getElementById('kb-category-filter');
|
||||
var catOptions = {
|
||||
1: 'О компании', 2: 'Аудит безопасности', 3: 'Сервисное обслуживание (SLA)',
|
||||
4: 'Реагирование на инциденты', 5: 'Технический надзор',
|
||||
6: 'Документация и нормативы', 7: 'Риск-инжиниринг', 8: 'Отрасли и кейсы'
|
||||
};
|
||||
categories = catOptions;
|
||||
Object.keys(catOptions).forEach(function(id){
|
||||
var opt1 = document.createElement('option');
|
||||
opt1.value = id; opt1.textContent = catOptions[id];
|
||||
catSelect.appendChild(opt1);
|
||||
var opt2 = document.createElement('option');
|
||||
opt2.value = id; opt2.textContent = catOptions[id];
|
||||
catFilter.appendChild(opt2);
|
||||
});
|
||||
});
|
||||
|
||||
loadKB();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,122 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.tab-bar { display:flex; gap:0; margin-bottom:20px; border-bottom:2px solid var(--border); }
|
||||
.tab-bar .tab { padding:10px 20px; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px; color:var(--text-muted); font-weight:500; }
|
||||
.tab-bar .tab.active { color:var(--accent); border-bottom-color:var(--accent); }
|
||||
.tab-content { display:none; }
|
||||
.tab-content.active { display:block; }
|
||||
.bot-settings-grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
|
||||
.bot-settings-grid .full-width { grid-column:1/-1; }
|
||||
.form-group { margin-bottom:16px; }
|
||||
.form-group label { display:block; font-weight:600; margin-bottom:6px; font-size:13px; color:var(--text-muted); }
|
||||
.form-group input, .form-group textarea, .form-group select {
|
||||
width:100%; padding:10px 14px; border:1px solid var(--border);
|
||||
border-radius:8px; font-size:14px; background:var(--bg-input); color:var(--text-primary);
|
||||
transition:border-color 0.2s;
|
||||
}
|
||||
.form-group input:focus, .form-group textarea:focus {
|
||||
outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(0,150,200,0.1);
|
||||
}
|
||||
.form-group textarea { min-height:100px; resize:vertical; }
|
||||
.form-actions { grid-column:1/-1; display:flex; gap:12px; margin-top:8px; }
|
||||
.form-actions .btn { min-width:160px; }
|
||||
.hint { font-size:12px; color:var(--text-muted); margin-top:4px; }
|
||||
.test-iframe { width:100%; height:650px; border:none; border-radius:8px; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Настройки чат-бота MAX — «София»</h2></div>
|
||||
<div class="tab-bar">
|
||||
<div class="tab active" onclick="switchTab('main')">Основные</div>
|
||||
<div class="tab" onclick="switchTab('test')">Тестовый прогон</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="tab-main">
|
||||
<form id="bot-settings-form">
|
||||
<div class="bot-settings-grid">
|
||||
<div class="form-group full-width">
|
||||
<label>Имя ассистента</label>
|
||||
<input type="text" name="assistant_name" id="assistant_name" placeholder="Например: София">
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>Роль ассистента</label>
|
||||
<textarea name="assistant_role" id="assistant_role" placeholder="Описание роли бота..."></textarea>
|
||||
<div class="hint">Будет использована для формирования системного промпта YandexGPT</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>MAX-бот ID</label>
|
||||
<input type="text" name="max_bot_id" id="max_bot_id" placeholder="Токен бота">
|
||||
<div class="hint">Токен чат-бота из платформы MAX</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email поддержки</label>
|
||||
<input type="email" name="support_email" id="support_email" placeholder="zakaz@aegisone.ru">
|
||||
<div class="hint">На этот email приходят уведомления о новых заявках</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>Тема письма</label>
|
||||
<input type="text" name="email_subject" id="email_subject" placeholder="Обращение через Виртуального помощника">
|
||||
<div class="hint">Тема email-уведомления об эскалации</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-test">
|
||||
<p style="margin-bottom:12px;color:var(--text-muted);font-size:13px;">Симуляция диалога с ботом. Отправляет запросы напрямую на вебхук бота.</p>
|
||||
<iframe src="/service/bot-test-content" class="test-iframe" id="test-iframe"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<div class="card-header"><h2>Общие настройки портала</h2></div>
|
||||
<div class="hint" style="margin-bottom:16px;">
|
||||
Yandex GPT и Yandex Disk настраиваются в разделе
|
||||
<a href="/service/portal-settings">Настройка портала</a>.
|
||||
Бот использует эти данные автоматически.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchTab(name){
|
||||
document.querySelectorAll('.tab-content').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.querySelectorAll('.tab-bar .tab').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
document.querySelector('.tab-bar .tab[onclick*="' + name + '"]').classList.add('active');
|
||||
if(name === 'test'){
|
||||
document.getElementById('test-iframe').src = '/service/bot-test-content';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('bot-settings-form').addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
var data = {};
|
||||
this.querySelectorAll('input, textarea').forEach(function(el){
|
||||
if(el.name) data[el.name] = el.value;
|
||||
});
|
||||
fetch('/service/api/bot/settings/save', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.ok) showNotification('Настройки сохранены', 'success');
|
||||
else showNotification('Ошибка: ' + (d.error || 'сохранения'), 'error');
|
||||
}).catch(function(){
|
||||
showNotification('Ошибка соединения', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
fetch('/service/api/bot/settings').then(function(r){ return r.json() }).then(function(d){
|
||||
['assistant_name','assistant_role','support_email','email_subject','max_bot_id'].forEach(function(k){
|
||||
var el = document.getElementById(k);
|
||||
if(el) el.value = d[k] || '';
|
||||
});
|
||||
if(document.getElementById('max_bot_id') && !document.getElementById('max_bot_id').value){
|
||||
document.getElementById('max_bot_id').value = 'f9LHodD0cOKkk03DYXj906MdIeRs...';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:'Inter','Segoe UI',sans-serif; background:#1B2838; color:#E8EDF2; font-size:13px; height:100vh; display:flex; flex-direction:column; }
|
||||
.test-container { display:flex; flex:1; gap:12px; padding:12px; overflow:hidden; }
|
||||
.test-chat { flex:1; border:1px solid #253248; border-radius:10px; display:flex; flex-direction:column; overflow:hidden; }
|
||||
.test-chat-header { background:#0A1628; padding:10px 14px; font-weight:600; border-bottom:1px solid #253248; font-size:12px; }
|
||||
.test-chat-messages { flex:1; overflow-y:auto; padding:10px 14px; }
|
||||
.test-chat-input { display:flex; border-top:1px solid #253248; }
|
||||
.test-chat-input input { flex:1; border:none; padding:10px 14px; font-size:13px; outline:none; background:#1B2838; color:#E8EDF2; }
|
||||
.test-chat-input button { padding:10px 16px; border:none; background:#00ADEF; color:#fff; cursor:pointer; font-weight:600; }
|
||||
.test-log { width:300px; border:1px solid #253248; border-radius:10px; padding:10px; overflow-y:auto; font-family:monospace; font-size:11px; line-height:1.5; background:#0A1628; }
|
||||
.test-log .log-entry { margin-bottom:4px; padding:3px 0; border-bottom:1px solid #1e3044; }
|
||||
.test-log .log-entry .time { color:#6B8299; }
|
||||
.test-log .log-entry .event { color:#00ADEF; font-weight:600; }
|
||||
.msg-bot { background:#1e3044; padding:7px 12px; border-radius:8px 8px 8px 0; margin-bottom:6px; max-width:85%; }
|
||||
.msg-user { background:#00ADEF; color:#fff; padding:7px 12px; border-radius:8px 8px 0 8px; margin-bottom:6px; max-width:85%; margin-left:auto; }
|
||||
.test-scenarios { display:flex; gap:6px; padding:8px 12px; flex-wrap:wrap; border-bottom:1px solid #253248; }
|
||||
.test-scenarios button { font-size:11px; padding:4px 10px; background:#253248; color:#E8EDF2; border:1px solid #3a4a60; border-radius:4px; cursor:pointer; }
|
||||
.test-scenarios button:hover { background:#00ADEF; color:#fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-scenarios">
|
||||
<button onclick="runScenario('greeting')">Приветствие</button>
|
||||
<button onclick="runScenario('consent_yes')">Согласие → заявка</button>
|
||||
<button onclick="runScenario('consent_no')">Отказ согласия</button>
|
||||
<button onclick="runScenario('question')">Общий вопрос</button>
|
||||
<button onclick="clearTest()">Очистить</button>
|
||||
</div>
|
||||
<div class="test-container">
|
||||
<div class="test-chat">
|
||||
<div class="test-chat-header">Чат с ботом (симуляция)</div>
|
||||
<div class="test-chat-messages" id="test-messages"></div>
|
||||
<div class="test-chat-input">
|
||||
<input type="text" id="test-input" placeholder="Введите сообщение..." onkeypress="if(event.key==='Enter') sendTestMessage()">
|
||||
<button onclick="sendTestMessage()">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="test-log" id="test-log"></div>
|
||||
</div>
|
||||
<script>
|
||||
var testUserId = 999999001;
|
||||
function log(event, detail){
|
||||
var logDiv = document.getElementById('test-log');
|
||||
var time = new Date().toLocaleTimeString();
|
||||
var entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
entry.innerHTML = '<span class="time">[' + time + ']</span> <span class="event">' + event + '</span> ' + (detail || '');
|
||||
logDiv.appendChild(entry);
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
function addMessage(text, from){
|
||||
var msgs = document.getElementById('test-messages');
|
||||
var div = document.createElement('div');
|
||||
div.className = from === 'bot' ? 'msg-bot' : 'msg-user';
|
||||
div.textContent = text;
|
||||
msgs.appendChild(div);
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
}
|
||||
function sendTestMessage(){
|
||||
var input = document.getElementById('test-input');
|
||||
var text = input.value.trim();
|
||||
if(!text) return;
|
||||
input.value = '';
|
||||
addMessage(text, 'user');
|
||||
log('message_created', 'Пользователь: ' + text);
|
||||
simulateWebhook('message_created', text);
|
||||
}
|
||||
function runScenario(scenario){
|
||||
clearTest();
|
||||
switch(scenario){
|
||||
case 'greeting':
|
||||
log('bot_started', 'Пользователь начал диалог');
|
||||
simulateWebhook('bot_started', null);
|
||||
break;
|
||||
case 'consent_yes':
|
||||
log('bot_started', 'Пользователь начал диалог');
|
||||
simulateWebhook('bot_started', null);
|
||||
setTimeout(function(){
|
||||
addMessage('Здравствуйте! Я хочу оставить заявку.', 'user');
|
||||
log('message_created', 'Пользователь: хочу оставить заявку');
|
||||
simulateWebhook('message_created', 'Здравствуйте! Я хочу оставить заявку.');
|
||||
}, 1000);
|
||||
break;
|
||||
case 'consent_no':
|
||||
log('bot_started', 'Пользователь начал диалог');
|
||||
simulateWebhook('bot_started', null);
|
||||
break;
|
||||
case 'question':
|
||||
log('bot_started', 'Пользователь начал диалог');
|
||||
simulateWebhook('bot_started', null);
|
||||
setTimeout(function(){
|
||||
addMessage('Какие услуги вы предоставляете?', 'user');
|
||||
log('message_created', 'Пользователь: какие услуги?');
|
||||
simulateWebhook('message_created', 'Какие услуги вы предоставляете?');
|
||||
}, 1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function simulateWebhook(type, text){
|
||||
var payload = {update_type: type};
|
||||
if(type === 'bot_started'){
|
||||
payload.user = {user_id: testUserId, first_name: 'Тест', last_name: 'Пользователь'};
|
||||
payload.chat_id = testUserId;
|
||||
} else if(type === 'message_created'){
|
||||
payload.sender = {user_id: testUserId};
|
||||
payload.chat_id = testUserId;
|
||||
payload.body = {text: text, attachments: []};
|
||||
}
|
||||
log('webhook', 'Отправка на /webhook (' + type + ')');
|
||||
fetch('/api/bot/webhook', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Max-Bot-Api-Secret': 'aegisone_bot_secret_2026',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
log('response', JSON.stringify(d));
|
||||
if(d && d.ok) log('success', 'Событие обработано');
|
||||
else log('error', 'Ошибка: ' + JSON.stringify(d));
|
||||
}).catch(function(err){
|
||||
log('error', 'Сетевая ошибка: ' + err);
|
||||
});
|
||||
}
|
||||
function clearTest(){
|
||||
document.getElementById('test-messages').innerHTML = '';
|
||||
document.getElementById('test-log').innerHTML = '';
|
||||
document.getElementById('test-input').value = '';
|
||||
log('info', 'Тест очищен');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,179 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.tickets-table { width:100%; border-collapse:collapse; }
|
||||
.tickets-table th, .tickets-table td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; }
|
||||
.tickets-table th { font-weight:600; color:var(--text-muted); background:var(--light-bg); }
|
||||
.tickets-table td { vertical-align:middle; }
|
||||
.status-badge { display:inline-block; padding:3px 10px; border-radius:12px; font-size:12px; font-weight:600; }
|
||||
.status-nova { background:#cce5ff; color:#004085; }
|
||||
.status-v-rabote { background:#fff3cd; color:#856404; }
|
||||
.status-zakryta { background:#d4edda; color:#155724; }
|
||||
.status-select { padding:4px 8px; border-radius:6px; border:1px solid var(--border); font-size:12px; }
|
||||
.ticket-detail-card { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:20px; margin-top:16px; display:none; }
|
||||
.ticket-detail-card.open { display:block; }
|
||||
.ticket-detail-card h3 { margin-bottom:12px; }
|
||||
.ticket-detail-card .field { margin-bottom:8px; }
|
||||
.ticket-detail-card .field label { font-weight:600; font-size:12px; color:var(--text-muted); display:block; }
|
||||
.ticket-detail-card .field .value { font-size:14px; }
|
||||
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; flex-wrap:wrap; }
|
||||
.filter-bar select, .filter-bar input { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h2>Заявки от чат-бота</h2>
|
||||
<span id="ticket-count" style="font-size:13px;color:var(--text-muted);">0 заявок</span>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<select id="status-filter" onchange="filterTickets()">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="Новая">Новая</option>
|
||||
<option value="В работе">В работе</option>
|
||||
<option value="Закрыта">Закрыта</option>
|
||||
</select>
|
||||
<input type="text" id="ticket-search" placeholder="Поиск..." oninput="filterTickets()">
|
||||
<button class="btn btn-sm" onclick="loadTickets()">Обновить</button>
|
||||
</div>
|
||||
|
||||
<table class="tickets-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Заголовок</th>
|
||||
<th>Приоритет</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tickets-body">
|
||||
<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="ticket-detail-card" id="ticket-detail">
|
||||
<h3 id="ticket-detail-title">Заявка #</h3>
|
||||
<div class="field"><label>Описание</label><div class="value" id="ticket-detail-desc"></div></div>
|
||||
<div class="field"><label>Приоритет</label><div class="value" id="ticket-detail-priority"></div></div>
|
||||
<div class="field"><label>Статус</label>
|
||||
<select id="ticket-detail-status" onchange="updateTicketStatus()">
|
||||
<option value="Новая">Новая</option>
|
||||
<option value="В работе">В работе</option>
|
||||
<option value="Закрыта">Закрыта</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Дата</label><div class="value" id="ticket-detail-date"></div></div>
|
||||
<button class="btn" onclick="closeDetail()">Закрыть</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var allTickets = [];
|
||||
var currentTicketId = null;
|
||||
|
||||
function loadTickets(){
|
||||
var tbody = document.getElementById('tickets-body');
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>';
|
||||
fetch('/api/bot/tickets').then(function(r){ return r.json() }).then(function(data){
|
||||
allTickets = data;
|
||||
document.getElementById('ticket-count').textContent = data.length + ' заявок';
|
||||
renderTickets(data);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTickets(tickets){
|
||||
var tbody = document.getElementById('tickets-body');
|
||||
if(!tickets.length){
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Нет заявок</td></tr>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
tickets.forEach(function(t){
|
||||
var statusClass = 'status-nova';
|
||||
if(t.status === 'В работе') statusClass = 'status-v-rabote';
|
||||
else if(t.status === 'Закрыта') statusClass = 'status-zakryta';
|
||||
var dateStr = t.created_at ? new Date(t.created_at).toLocaleString() : '';
|
||||
html += '<tr>' +
|
||||
'<td>' + t.id + '</td>' +
|
||||
'<td><a href="javascript:void(0)" onclick="showDetail(' + t.id + ')">' + escapeHtml(t.title) + '</a></td>' +
|
||||
'<td>' + t.priority + '</td>' +
|
||||
'<td><span class="status-badge ' + statusClass + '">' + t.status + '</span></td>' +
|
||||
'<td style="font-size:12px;color:var(--text-muted);">' + dateStr + '</td>' +
|
||||
'<td>' +
|
||||
' <select class="status-select" onchange="changeStatus(' + t.id + ', this.value)">' +
|
||||
' <option value="">Сменить статус</option>' +
|
||||
' <option value="Новая">Новая</option>' +
|
||||
' <option value="В работе">В работе</option>' +
|
||||
' <option value="Закрыта">Закрыта</option>' +
|
||||
' </select>' +
|
||||
'</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function filterTickets(){
|
||||
var statusFilter = document.getElementById('status-filter').value;
|
||||
var search = document.getElementById('ticket-search').value.toLowerCase();
|
||||
var filtered = allTickets.filter(function(t){
|
||||
if(statusFilter && t.status !== statusFilter) return false;
|
||||
if(search && !t.title.toLowerCase().includes(search) && !(t.description || '').toLowerCase().includes(search)) return false;
|
||||
return true;
|
||||
});
|
||||
renderTickets(filtered);
|
||||
}
|
||||
|
||||
function showDetail(id){
|
||||
currentTicketId = id;
|
||||
var ticket = allTickets.find(function(t){ return t.id === id; });
|
||||
if(!ticket) return;
|
||||
document.getElementById('ticket-detail-title').textContent = 'Заявка #' + ticket.id;
|
||||
document.getElementById('ticket-detail-desc').textContent = ticket.description || '—';
|
||||
document.getElementById('ticket-detail-priority').textContent = ticket.priority;
|
||||
document.getElementById('ticket-detail-status').value = ticket.status;
|
||||
document.getElementById('ticket-detail-date').textContent = ticket.created_at ? new Date(ticket.created_at).toLocaleString() : '—';
|
||||
document.getElementById('ticket-detail').classList.add('open');
|
||||
}
|
||||
|
||||
function closeDetail(){
|
||||
document.getElementById('ticket-detail').classList.remove('open');
|
||||
currentTicketId = null;
|
||||
}
|
||||
|
||||
function changeStatus(id, status){
|
||||
if(!status) return;
|
||||
updateTicketStatusApi(id, status);
|
||||
}
|
||||
|
||||
function updateTicketStatus(){
|
||||
if(currentTicketId){
|
||||
var status = document.getElementById('ticket-detail-status').value;
|
||||
updateTicketStatusApi(currentTicketId, status);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTicketStatusApi(id, status){
|
||||
fetch('/api/bot/tickets/' + id, {
|
||||
method: 'PATCH',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({status: status})
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.ok){
|
||||
loadTickets();
|
||||
if(currentTicketId === id) showDetail(id);
|
||||
} else {
|
||||
showNotification('Ошибка: ' + (d.error || 'неизвестная'), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(s){
|
||||
if(!s) return '';
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
loadTickets();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,281 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.tab-bar { display:flex; gap:0; margin-bottom:24px; border-bottom:2px solid var(--border); }
|
||||
.tab-bar .tab { padding:10px 20px; cursor:pointer; border-bottom:2px solid transparent;
|
||||
margin-bottom:-2px; color:var(--text-muted); font-weight:500; }
|
||||
.tab-bar .tab.active { color:var(--accent); border-bottom-color:var(--accent); }
|
||||
.tab-content { display:none; }
|
||||
.tab-content.active { display:block; }
|
||||
.formula-test-panel { margin-top: 16px; }
|
||||
.formula-description {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.formula-code {
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
color: var(--accent);
|
||||
}
|
||||
.formula-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.formula-result {
|
||||
margin-top: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.formula-result-success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
.formula-result-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
.accordion { margin-bottom:8px; }
|
||||
.accordion-header {
|
||||
display:flex; justify-content:space-between; align-items:center;
|
||||
padding:10px 14px; background:var(--bg-input); border:1px solid var(--border);
|
||||
border-radius:var(--radius); cursor:pointer; user-select:none;
|
||||
}
|
||||
.accordion-header:hover { background:var(--bg-secondary); }
|
||||
.accordion-header-left { display:flex; align-items:center; gap:8px; }
|
||||
.accordion-arrow { font-size:10px; color:var(--text-muted); width:12px; }
|
||||
.accordion-title { font-weight:600; font-size:13px; }
|
||||
.accordion-badge {
|
||||
font-size:11px; background:var(--accent); color:#fff;
|
||||
border-radius:10px; padding:1px 8px;
|
||||
}
|
||||
.accordion-header-right { display:flex; align-items:center; gap:8px; }
|
||||
.accordion-formula, .accordion-action {
|
||||
cursor:pointer; font-size:16px; opacity:0.7;
|
||||
}
|
||||
.accordion-formula:hover, .accordion-action:hover { opacity:1; }
|
||||
.accordion-body { display:none; padding:12px 0 4px; }
|
||||
</style>
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Формулы</h2></div>
|
||||
<div class="tab-bar">
|
||||
<div class="tab active" onclick="switchTab('test')">Тест формул</div>
|
||||
<div class="tab" onclick="switchTab('coeffs')">Коэффициенты</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="tab-test">
|
||||
<div class="form-group">
|
||||
<label>Выберите формулу для тестирования</label>
|
||||
<select id="formula-select" onchange="switchFormula(this.value)" style="max-width:400px">
|
||||
{% for f in formulas %}
|
||||
<option value="{{ f.key }}" {% if f.key == selected %}selected{% endif %}>{{ f.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% for f in formulas %}
|
||||
<div id="formula-{{ f.key }}" class="formula-test-panel" {% if f.key != selected %}style="display:none"{% endif %}>
|
||||
<div class="formula-description">
|
||||
<div style="margin-bottom:8px"><strong>Формула:</strong></div>
|
||||
<div class="formula-code">{{ f.formula }}</div>
|
||||
<div style="margin-top:8px"><strong>Результат:</strong> {{ f.result }}</div>
|
||||
</div>
|
||||
|
||||
{% if f.sandbox %}
|
||||
<form class="formula-sandbox-form" onsubmit="return testFormula(event, '{{ f.key }}')">
|
||||
<h3 style="margin:16px 0 12px;font-size:14px;">Параметры расчёта</h3>
|
||||
<div class="formula-inputs">
|
||||
{% for inp in f.sandbox.inputs %}
|
||||
<div class="form-group">
|
||||
<label>{{ inp.label }}</label>
|
||||
{% if inp.type == "checkbox" %}
|
||||
<input type="checkbox" name="{{ inp.key }}" value="1" style="width:20px;height:20px;accent-color:var(--accent)">
|
||||
{% elif inp.type == "select" %}
|
||||
<select name="{{ inp.key }}">
|
||||
{% for ok, ol in inp.options.items() %}
|
||||
<option value="{{ ok }}" {% if ok == inp.default %}selected{% endif %}>{{ ol }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<input type="number" name="{{ inp.key }}" value="{{ inp.default|default(0) }}" step="any">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Рассчитать</button>
|
||||
<div class="formula-result" id="result-{{ f.key }}" style="display:none"></div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="empty-state" style="padding:24px 0">
|
||||
<p>Для этой формулы пока нет тестовой песочницы.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3 style="margin:20px 0 10px;font-size:13px;color:var(--text-muted)">Связанные коэффициенты</h3>
|
||||
<div class="table-wrap"><table><thead><tr><th>Ключ</th><th>Значение</th><th>Описание</th></tr></thead>
|
||||
<tbody>{% for c in f.coeffs %}<tr>
|
||||
<td><code>{{ c.key }}</code></td>
|
||||
<td><strong>{{ c.value }}</strong></td>
|
||||
<td><small>{{ c.description }}</small></td>
|
||||
</tr>{% endfor %}</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-coeffs">
|
||||
{% if groups|length == 0 %}
|
||||
<div class="card-body"><p class="empty">Нет коэффициентов</p></div>
|
||||
{% else %}
|
||||
{% for gkey, gdata in groups.items() %}
|
||||
<div class="accordion">
|
||||
<div class="accordion-header" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header-left">
|
||||
<span class="accordion-arrow">▶</span>
|
||||
<span class="accordion-title">{{ gdata.info.name }}</span>
|
||||
<span class="accordion-badge">{{ gdata.coeffs|length }}</span>
|
||||
</div>
|
||||
<div class="accordion-header-right">
|
||||
<span class="accordion-formula" title="Формула" onclick="event.stopPropagation(); showFormulaPopover(this, '{{ gkey }}')" data-formula="{{ gdata.info.formula }}" data-result="{{ gdata.info.result }}">📐</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-body">
|
||||
<div class="table-wrap"><table><thead><tr><th>Ключ</th><th>Значение</th><th>Описание</th><th>Формула</th><th>Активен</th><th class="actions"></th></tr></thead>
|
||||
<tbody>{% for c in gdata.coeffs %}<tr>
|
||||
<td><code>{{ c.key }}</code></td>
|
||||
<td><strong>{{ c.value }}</strong></td>
|
||||
<td><small>{{ c.description }}</small></td>
|
||||
<td><small>{{ c.formula_ref }}</small></td>
|
||||
<td><form method="post" action="/service/coefficients/toggle"><input type="hidden" name="id" value="{{ c.id }}"><button class="btn btn-{{ 'success' if c.is_active else 'secondary' }} btn-sm">{{ '✓' if c.is_active else '✕' }}</button></form></td>
|
||||
<td class="actions"><button class="btn btn-secondary btn-sm" onclick="editCoeff({{ c.id }},'{{ c.key|e }}','{{ c.value }}','{{ c.description|e }}','{{ c.formula_ref|e }}')">✎</button>
|
||||
<form method="post" action="/service/coefficients/delete" style="display:inline" onsubmit="return confirm('Удалить коэффициент?')"><input type="hidden" name="id" value="{{ c.id }}"><button class="btn btn-danger btn-sm">✕</button></form>
|
||||
</td>
|
||||
</tr>{% endfor %}</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div style="margin-top:16px;">
|
||||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('modal-coeff').classList.add('open')">+ Добавить коэффициент</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coefficient edit modal -->
|
||||
<div class="modal-overlay" id="modal-coeff"><div class="modal">
|
||||
<button type="button" class="modal-close" onclick="this.closest('.modal-overlay').classList.remove('open')">✕</button>
|
||||
<h2 id="modal-coeff-title">Новый коэффициент</h2>
|
||||
<form method="post" id="modal-coeff-form" action="/service/coefficients/create">
|
||||
<input type="hidden" name="id" id="coeff-id">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Ключ</label><input type="text" name="key" id="coeff-key" required></div>
|
||||
<div class="form-group"><label>Значение</label><input type="text" name="value" id="coeff-value" required></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Описание</label><input type="text" name="description" id="coeff-description"></div>
|
||||
<div class="form-group"><label>Формула</label><input type="text" name="formula_ref" id="coeff-formula_ref"></div>
|
||||
<div class="form-actions"><button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button></div>
|
||||
</form>
|
||||
</div></div>
|
||||
|
||||
<!-- Formula info modal -->
|
||||
<div class="modal-overlay" id="modal-formula-info"><div class="modal modal-sm">
|
||||
<button type="button" class="modal-close" onclick="document.getElementById('modal-formula-info').classList.remove('open')">✕</button>
|
||||
<h2 id="formula-info-title">Формула</h2>
|
||||
<div id="formula-info-content" style="margin-top:12px"></div>
|
||||
</div></div>
|
||||
|
||||
<script>
|
||||
function switchTab(name){
|
||||
document.querySelectorAll('.tab-content').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
document.querySelector('.tab[onclick*="' + name + '"]').classList.add('active');
|
||||
}
|
||||
|
||||
function switchFormula(key) {
|
||||
document.querySelectorAll('.formula-test-panel').forEach(function(el) {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
var panel = document.getElementById('formula-' + key);
|
||||
if (panel) panel.style.display = 'block';
|
||||
}
|
||||
|
||||
function testFormula(event, key) {
|
||||
event.preventDefault();
|
||||
var form = event.target;
|
||||
var data = new URLSearchParams(new FormData(form));
|
||||
var resultBox = document.getElementById('result-' + key);
|
||||
resultBox.style.display = 'block';
|
||||
resultBox.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
resultBox.className = 'formula-result';
|
||||
fetch('/service/coefficients/test/' + key, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: data.toString()
|
||||
}).then(function(r) { return r.json() }).then(function(resp) {
|
||||
resultBox.textContent = 'Результат: ' + (resp.result || 'Ошибка');
|
||||
resultBox.className = 'formula-result formula-result-success';
|
||||
}).catch(function(err) {
|
||||
resultBox.textContent = 'Ошибка: ' + err.message;
|
||||
resultBox.className = 'formula-result formula-result-error';
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function toggleAccordion(el) {
|
||||
var arrow = el.querySelector('.accordion-arrow');
|
||||
var body = el.nextElementSibling;
|
||||
if (body.style.display === 'none' || body.style.display === '') {
|
||||
body.style.display = 'block';
|
||||
arrow.textContent = '▼';
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
arrow.textContent = '▶';
|
||||
}
|
||||
}
|
||||
|
||||
function editCoeff(id,key,value,desc,ref){
|
||||
document.getElementById('modal-coeff').classList.add('open');
|
||||
document.getElementById('modal-coeff-title').textContent = 'Редактировать коэффициент';
|
||||
document.getElementById('modal-coeff-form').action = '/service/coefficients/edit';
|
||||
document.getElementById('coeff-id').value = id;
|
||||
document.getElementById('coeff-key').value = key;
|
||||
document.getElementById('coeff-value').value = value;
|
||||
document.getElementById('coeff-description').value = desc;
|
||||
document.getElementById('coeff-formula_ref').value = ref;
|
||||
}
|
||||
|
||||
function showFormulaPopover(el, gkey) {
|
||||
var formula = el.getAttribute('data-formula');
|
||||
var result = el.getAttribute('data-result');
|
||||
var title = el.closest('.accordion-header').querySelector('.accordion-title').textContent;
|
||||
document.getElementById('formula-info-title').textContent = title;
|
||||
document.getElementById('formula-info-content').innerHTML =
|
||||
'<div style="margin-bottom:12px"><strong>Формула:</strong></div>' +
|
||||
'<div style="background:var(--bg-input);padding:12px;border-radius:8px;font-family:monospace;font-size:13px;margin-bottom:12px;white-space:pre-wrap">' + formula + '</div>' +
|
||||
'<div><strong>Результат:</strong> ' + result + '</div>';
|
||||
document.getElementById('modal-formula-info').classList.add('open');
|
||||
}
|
||||
|
||||
// Open first accordion by default
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var first = document.querySelector('.accordion-body');
|
||||
if (first) { first.style.display = 'block'; first.previousElementSibling.querySelector('.accordion-arrow').textContent = '▼'; }
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.tab-bar { display:flex; gap:0; margin-bottom:24px; border-bottom:2px solid var(--border); }
|
||||
.tab-bar .tab { padding:10px 20px; cursor:pointer; border-bottom:2px solid transparent;
|
||||
margin-bottom:-2px; color:var(--text-muted); font-weight:500; }
|
||||
.tab-bar .tab.active { color:var(--accent); border-bottom-color:var(--accent); }
|
||||
.tab-content { display:none; }
|
||||
.tab-content.active { display:block; }
|
||||
</style>
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Настройка портала</h2></div>
|
||||
<div class="tab-bar">
|
||||
<div class="tab active" onclick="switchTab('general')">Общие</div>
|
||||
<div class="tab" onclick="switchTab('documents')">Управление документами</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="tab-general">
|
||||
<form id="portal-settings-form" class="form-grid">
|
||||
<h3 style="grid-column:1/-1;margin:0 0 8px;">Yandex GPT</h3>
|
||||
<div class="form-group"><label>Ключ YandexGPT (пусто = ключевые слова)</label>
|
||||
<input type="text" name="yandex_gpt_key" id="yandex_gpt_key" placeholder="Api-Key ..."></div>
|
||||
<div class="form-group"><label>Yandex Folder ID</label>
|
||||
<input type="text" name="yandex_folder_id" id="yandex_folder_id" placeholder="b1g..."></div>
|
||||
|
||||
<h3 style="grid-column:1/-1;margin:16px 0 8px;">Yandex Disk</h3>
|
||||
<div class="form-group"><label>OAuth-токен Яндекс.Диска</label>
|
||||
<input type="text" name="yandex_disk_token" id="yandex_disk_token"></div>
|
||||
<div class="form-group"><label>Корневая папка</label>
|
||||
<input type="text" name="yandex_disk_root" id="yandex_disk_root" placeholder="/AegisOne_Service"></div>
|
||||
<div class="form-group"><label>Client ID</label>
|
||||
<input type="text" name="yandex_disk_client_id" id="yandex_disk_client_id"></div>
|
||||
<div class="form-group"><label>Client Secret</label>
|
||||
<input type="text" name="yandex_disk_client_secret" id="yandex_disk_client_secret"></div>
|
||||
<div class="form-actions" style="grid-column:1/-1;">
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-documents">
|
||||
<p>Управление доступом к документам:</p>
|
||||
<a href="/service/documents/admin/permissions" class="btn btn-primary">Перейти к управлению документами</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function switchTab(name){
|
||||
document.querySelectorAll('.tab-content').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
document.querySelector('.tab[onclick*="' + name + '"]').classList.add('active');
|
||||
}
|
||||
document.getElementById('portal-settings-form').addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
var data = {};
|
||||
this.querySelectorAll('input').forEach(function(el){
|
||||
if(el.name) data[el.name] = el.value;
|
||||
});
|
||||
fetch('/service/api/bot/settings/save', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.ok) showNotification('Настройки сохранены', 'success');
|
||||
else showNotification('Ошибка сохранения', 'error');
|
||||
});
|
||||
});
|
||||
fetch('/service/api/bot/settings').then(function(r){ return r.json() }).then(function(d){
|
||||
['yandex_gpt_key','yandex_folder_id','yandex_disk_token','yandex_disk_root',
|
||||
'yandex_disk_client_id','yandex_disk_client_secret'].forEach(function(k){
|
||||
var el = document.getElementById(k);
|
||||
if(el) el.value = d[k] || '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,126 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.tab-bar { display:flex; gap:0; margin-bottom:24px; border-bottom:2px solid var(--border); }
|
||||
.tab-bar .tab { padding:10px 20px; cursor:pointer; border-bottom:2px solid transparent;
|
||||
margin-bottom:-2px; color:var(--text-muted); font-weight:500; }
|
||||
.tab-bar .tab.active { color:var(--accent); border-bottom-color:var(--accent); }
|
||||
.tab-content { display:none; }
|
||||
.tab-content.active { display:block; }
|
||||
.menu-group { margin-bottom:16px; }
|
||||
.menu-group h4 { margin:0 0 8px; color:var(--text-muted); font-size:13px; text-transform:uppercase; }
|
||||
.menu-item { display:flex; align-items:center; gap:8px; padding:8px 12px;
|
||||
border:1px solid var(--border); border-radius:6px; margin-bottom:4px; cursor:pointer; }
|
||||
.menu-item:hover { background:var(--bg-secondary); }
|
||||
.menu-item input[type="checkbox"] { width:18px; height:18px; cursor:pointer; }
|
||||
.menu-item .label { flex:1; }
|
||||
</style>
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Настройка Ролей</h2></div>
|
||||
<div class="tab-bar">
|
||||
<div class="tab active" onclick="switchTab('engineer')">Инженер</div>
|
||||
<div class="tab" onclick="switchTab('technician')">Техник</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="tab-engineer">
|
||||
<form id="role-form-engineer" class="menu-group">
|
||||
<p style="margin:0 0 12px;color:var(--text-muted);">Отметьте пункты меню, доступные инженеру:</p>
|
||||
<div id="menu-engineer"></div>
|
||||
<div class="form-actions" style="margin-top:16px;">
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-technician">
|
||||
<form id="role-form-technician" class="menu-group">
|
||||
<p style="margin:0 0 12px;color:var(--text-muted);">Отметьте пункты меню, доступные технику:</p>
|
||||
<div id="menu-technician"></div>
|
||||
<div class="form-actions" style="margin-top:16px;">
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var MENU_ITEMS = [
|
||||
// Панель
|
||||
{key: 'charts', label: 'Графики', section: 'Панель'},
|
||||
{key: 'ceo', label: 'CEO дашборд', section: 'Панель'},
|
||||
// Управление
|
||||
{key: 'customers', label: 'Клиенты', section: 'Управление'},
|
||||
{key: 'objects', label: 'Объекты', section: 'Управление'},
|
||||
{key: 'sla', label: 'SLA контракты', section: 'Управление'},
|
||||
{key: 'questionnaire', label: 'Опросник', section: 'Управление'},
|
||||
{key: 'passports', label: 'Паспорта объектов', section: 'Управление'},
|
||||
// Работа
|
||||
{key: 'users', label: 'Сотрудники', section: 'Работа'},
|
||||
{key: 'assignments', label: 'Назначение сотрудников', section: 'Работа'},
|
||||
{key: 'docs', label: 'Просмотр документации', section: 'Работа'},
|
||||
{key: 'tasks', label: 'Задачи', section: 'Работа'},
|
||||
{key: 'reports', label: 'Отчёты', section: 'Работа'},
|
||||
{key: 'incidents', label: 'Инциденты', section: 'Работа'},
|
||||
{key: 'checklist', label: 'Чек-лист', section: 'Работа'},
|
||||
{key: 'tech_access', label: 'Доступ техников к документам', section: 'Работа'},
|
||||
// Контент
|
||||
{key: 'blog', label: 'Управление блогом', section: 'Контент'},
|
||||
{key: 'cases', label: 'Примеры из практики', section: 'Контент'},
|
||||
];
|
||||
|
||||
function switchTab(name){
|
||||
document.querySelectorAll('.tab-content').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); });
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
document.querySelector('.tab[onclick*="' + name + '"]').classList.add('active');
|
||||
}
|
||||
|
||||
function loadPermissions(){
|
||||
fetch('/service/api/role-permissions').then(function(r){ return r.json() }).then(function(data){
|
||||
['engineer','technician'].forEach(function(role){
|
||||
var container = document.getElementById('menu-' + role);
|
||||
container.innerHTML = '';
|
||||
var permMap = {};
|
||||
(data[role] || []).forEach(function(p){ permMap[p.menu_key] = p.visible; });
|
||||
var currentSection = '';
|
||||
MENU_ITEMS.forEach(function(item){
|
||||
if(item.section !== currentSection){
|
||||
currentSection = item.section;
|
||||
var hdr = document.createElement('h4');
|
||||
hdr.textContent = item.section;
|
||||
container.appendChild(hdr);
|
||||
}
|
||||
var div = document.createElement('div');
|
||||
div.className = 'menu-item';
|
||||
var checked = permMap[item.key] !== false;
|
||||
div.innerHTML = '<input type="checkbox" name="' + item.key + '" ' + (checked?'checked':'') + '>' +
|
||||
'<span class="label">' + item.label + '</span>';
|
||||
div.querySelector('input[type="checkbox"]').addEventListener('change', function(){
|
||||
div.style.opacity = this.checked ? '1' : '0.5';
|
||||
});
|
||||
div.style.opacity = checked ? '1' : '0.5';
|
||||
container.appendChild(div);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
['engineer','technician'].forEach(function(role){
|
||||
document.getElementById('role-form-' + role).addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
var perms = [];
|
||||
this.querySelectorAll('input[type="checkbox"]').forEach(function(cb){
|
||||
perms.push({menu_key: cb.name, visible: cb.checked});
|
||||
});
|
||||
fetch('/service/api/role-permissions/save', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({role: role, permissions: perms})
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.ok) alert('Настройки роли "' + role + '" сохранены');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
loadPermissions();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -5,15 +5,16 @@
|
||||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('modal-user').classList.add('open')">+ Добавить</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>Логин</th><th>ФИО</th><th>Роль</th><th>Телефон</th><th>Email</th><th>Статус</th><th>Создан</th><th class="actions"></th></tr></thead>
|
||||
<table><thead><tr><th>Логин</th><th>ФИО</th><th>Роль</th><th>Телефон</th><th>Email</th><th>MAX ID</th><th>Статус</th><th>Создан</th><th class="actions"></th></tr></thead>
|
||||
<tbody>{% for u in users %}<tr>
|
||||
<td>{{ u.login }}</td><td>{{ u.full_name }}</td>
|
||||
<td><span class="badge badge-{{ u.role }}">{{ {'owner':'Владелец','engineer':'Инженер','technician':'Техник'}[u.role] }}</span></td>
|
||||
<td>{{ u.phone }}</td><td>{{ u.email }}</td>
|
||||
<td>{{ u.max_user_id or '—' }}</td>
|
||||
<td><span class="badge badge-{{ 'active' if u.is_active else 'inactive' }}">{{ 'Активен' if u.is_active else 'Неактивен' }}</span></td>
|
||||
<td>{{ u.created_at.strftime('%d.%m.%Y') if u.created_at else '' }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-secondary btn-sm" onclick="editUser({{ u.id }},'{{ u.login }}','{{ u.full_name }}','{{ u.role }}','{{ u.phone }}','{{ u.email }}',{{ 'true' if u.is_active else 'false' }})">✎</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="editUser({{ u.id }},'{{ u.login }}','{{ u.full_name }}','{{ u.role }}','{{ u.phone }}','{{ u.email }}','{{ u.max_user_id or '' }}',{{ 'true' if u.is_active else 'false' }})">✎</button>
|
||||
{% if u.role != 'owner' %}
|
||||
<form method="post" action="/service/users/delete" style="display:inline" onsubmit="return confirm('Удалить пользователя?')">
|
||||
<input type="hidden" name="id" value="{{ u.id }}">
|
||||
@@ -41,6 +42,9 @@
|
||||
<div class="form-group"><label>Телефон</label><input type="text" name="phone" id="user-phone"></div>
|
||||
<div class="form-group"><label>Email</label><input type="email" name="email" id="user-email"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>MAX ID (мессенджер)</label><input type="number" name="max_user_id" id="user-max_id" placeholder="ID пользователя в MAX"></div>
|
||||
</div>
|
||||
<div class="form-check"><input type="checkbox" name="is_active" id="user-is_active" value="1" checked><label for="user-is_active">Активен</label></div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
@@ -50,7 +54,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function editUser(id,login,name,role,phone,email,active){
|
||||
function editUser(id,login,name,role,phone,email,maxId,active){
|
||||
document.getElementById('modal-user').classList.add('open');
|
||||
document.getElementById('modal-user-title').textContent = 'Редактировать сотрудника';
|
||||
document.getElementById('modal-user-form').action = '/service/users/edit';
|
||||
@@ -59,6 +63,7 @@ function editUser(id,login,name,role,phone,email,active){
|
||||
document.getElementById('user-full_name').value = name;
|
||||
document.getElementById('user-role').value = role;
|
||||
document.getElementById('user-phone').value = phone; document.getElementById('user-email').value = email;
|
||||
document.getElementById('user-max_id').value = maxId || '';
|
||||
document.getElementById('user-is_active').checked = active;
|
||||
document.getElementById('user-password').required = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo " AegisOne Documents Module Deploy"
|
||||
echo "=========================================="
|
||||
|
||||
REMOTE="root@81.177.141.34"
|
||||
REMOTE_DIR="/opt/projects/aegisone-py/app"
|
||||
|
||||
echo "[1/4] Копирование documents_router.py..."
|
||||
scp app/documents_router.py $REMOTE:$REMOTE_DIR/
|
||||
echo " ✅ documents_router.py скопирован"
|
||||
|
||||
echo "[2/4] Копирование шаблонов документов..."
|
||||
ssh $REMOTE "mkdir -p $REMOTE_DIR/templates/documents"
|
||||
scp app/templates/documents/list.html $REMOTE:$REMOTE_DIR/templates/documents/
|
||||
scp app/templates/documents/view.html $REMOTE:$REMOTE_DIR/templates/documents/
|
||||
scp app/templates/documents/edit.html $REMOTE:$REMOTE_DIR/templates/documents/
|
||||
scp app/templates/documents/permissions.html $REMOTE:$REMOTE_DIR/templates/documents/
|
||||
echo " ✅ Шаблоны скопированы"
|
||||
|
||||
echo "[3/4] Копирование docs и permissions..."
|
||||
ssh $REMOTE "mkdir -p /opt/projects/aegisone-py/docs /opt/projects/aegisone-py/permissions"
|
||||
scp -r docs/* $REMOTE:/opt/projects/aegisone-py/docs/
|
||||
echo " ✅ Документы скопированы"
|
||||
|
||||
echo "[4/4] Обновление main.py и перезапуск..."
|
||||
scp app/main.py $REMOTE:$REMOTE_DIR/
|
||||
scp app/services/docs_permissions.py $REMOTE:$REMOTE_DIR/services/
|
||||
|
||||
ssh $REMOTE "cd /opt/projects/aegisone-py && docker compose build --no-cache app && docker compose up -d app"
|
||||
echo " ✅ Приложение пересобрано и запущено"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Готово! Проверка..."
|
||||
echo "=========================================="
|
||||
sleep 5
|
||||
ssh $REMOTE "curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:8000/service/documents/"
|
||||
echo ""
|
||||
echo "URL: https://service.aegisone.ru/service/documents/"
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd /opt/projects
|
||||
|
||||
BACKUP="aegisone-py-backup-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "Backing up current to $BACKUP..."
|
||||
mv aegisone-py "$BACKUP" 2>/dev/null || true
|
||||
|
||||
echo "Extracting new version..."
|
||||
mkdir -p aegisone-py
|
||||
cd aegisone-py
|
||||
unzip -q ../aegisone-py-deploy.zip
|
||||
|
||||
echo "Building and restarting..."
|
||||
docker compose build --no-cache --network host
|
||||
docker compose up -d
|
||||
|
||||
echo "Deploy complete"
|
||||
@@ -1,9 +1,8 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: aegisone-postgres
|
||||
network_mode: host
|
||||
environment:
|
||||
POSTGRES_DB: aegisone
|
||||
POSTGRES_USER: aegisone
|
||||
@@ -12,42 +11,36 @@ services:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./sql/schema_postgresql.sql:/docker-entrypoint-initdb.d/01_schema.sql
|
||||
- ./sql/seed_data.sql:/docker-entrypoint-initdb.d/02_seed.sql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- aegisone-net
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U aegisone"]
|
||||
test: ["CMD-SHELL", "pg_isready -U aegisone -h localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
network: host
|
||||
container_name: aegisone-app
|
||||
network_mode: host
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://aegisone:aegisone_pass@postgres:5432/aegisone
|
||||
DATABASE_URL: postgresql+asyncpg://aegisone:aegisone_pass@localhost:5432/aegisone
|
||||
SECRET_KEY: ${SECRET_KEY:-change-me-to-random-string}
|
||||
SESSION_TTL: 3600
|
||||
APP_ENV: production
|
||||
LOG_LEVEL: info
|
||||
PORTAINER_URL: ${PORTAINER_URL:-http://81.177.141.34:9001}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./docs:/app/docs
|
||||
- ./permissions:/app/permissions
|
||||
- ./uploads:/app/uploads
|
||||
networks:
|
||||
- aegisone-net
|
||||
- ./CHANGELOG.md:/app/CHANGELOG.md
|
||||
- ./version.txt:/app/version.txt
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
|
||||
networks:
|
||||
aegisone-net:
|
||||
driver: bridge
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
# AegisOne Engineering — План миграции инфраструктуры и переписывания сервисной части
|
||||
|
||||
> **Статус:** План утверждён, готов к реализации
|
||||
> **Дата:** 2026-05-17
|
||||
> **Команда:** Владелец + AI-ассистент
|
||||
> **Примечание:** Если при реализации возникнут вопросы — лучше уточнить, чем переделывать потом.
|
||||
|
||||
---
|
||||
|
||||
## 1. Текущее состояние
|
||||
|
||||
### 1.1. Публичная часть (aegisone.ru)
|
||||
- **Технологии:** PHP 7.4, MySQL, inline CSS/JS
|
||||
- **Хостинг:** Shared-хостинг (проблемы: HTTP 302-редирект, блокировка `/assets/`, отключённый `RewriteEngine`, PHP 7.4 тупик)
|
||||
- **Страницы:** Главная, услуги, блог, контакты, форма обратной связи
|
||||
- **SEO:** `robots.txt`, `sitemap.xml`, канонические URL
|
||||
- **Решение:** **Не трогать.** Перенести на VPS как есть в Docker-контейнер.
|
||||
|
||||
### 1.2. Сервисная часть (service.aegisone.ru)
|
||||
- **Технологии:** PHP 7.4, MySQL, inline CSS/JS
|
||||
- **Функционал:** Админ-панель (управление пользователями, объектами, SLA, опросниками, блогом, документами), расчёты (Risk Score, Object Index, SLA price), паспорта объектов
|
||||
- **Проблемы:** PHP 7.4 ограничивает развитие, нет автоматизации отчётов, нет интеграций
|
||||
- **Решение:** **Полностью переписать на Python/FastAPI + Jinja2**
|
||||
|
||||
### 1.3. Инфраструктура
|
||||
- **VPS:** Уже есть, на нём один проект (FastAPI + Uvicorn + Celery + Nginx + PostgreSQL + Redis + Alembic)
|
||||
- **Хостинг:** Платный, планируется отказ
|
||||
- **Git:** Нет централизованного сервера, работа на локальной машине
|
||||
- **SSL:** От регистратора домена (статический, неудобно для мультидомена)
|
||||
- **Домены:** `aegisone.ru` (публичная часть), планируется `service.aegisone.ru` (админка)
|
||||
|
||||
---
|
||||
|
||||
## 2. Целевая архитектура
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ VPS │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Nginx (reverse proxy + SSL) │ │
|
||||
│ │ порт 80/443, Let's Encrypt │ │
|
||||
│ │ │ │
|
||||
│ │ aegisone.ru ──────────────► php-fpm container (порт 9000) │ │
|
||||
│ │ service.aegisone.ru ─────► fastapi container (порт 8000) │ │
|
||||
│ │ other-project.ru ────────► project2 container (порт 8001) │ │
|
||||
│ │ git.aegisone.ru ─────────► gitea container (порт 3000) │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ AegisOne PHP │ │ AegisOne Python │ │ Other Project │ │
|
||||
│ │ (публичная) │ │ (админка) │ │ (FastAPI) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • Главная │ │ • Аутентификация│ │ • ... │ │
|
||||
│ │ • Услуги │ │ • Пользователи │ │ │ │
|
||||
│ │ • Блог │ │ • Объекты │ │ │ │
|
||||
│ │ • Контакты │ │ • Клиенты │ │ │ │
|
||||
│ │ • FAQ │ │ • SLA │ │ │ │
|
||||
│ │ • Карусель │ │ • Опросники │ │ │ │
|
||||
│ │ • Форма │ │ • Расчёты │ │ │ │
|
||||
│ │ │ │ • Отчёты (PDF) │ │ │ │
|
||||
│ │ │ │ • Документы │ │ │ │
|
||||
│ │ │ │ • Yandex Disk │ │ │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────────┬──────────┘ │ │
|
||||
│ │ │ │
|
||||
│ ┌──────────▼──────────┐ ┌─────────▼────────┐ │
|
||||
│ │ PostgreSQL │ │ PostgreSQL │ │
|
||||
│ │ (AegisOne БД) │ │ (Project 2 БД) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • users │ │ • ... │ │
|
||||
│ │ • customers │ │ │ │
|
||||
│ │ • objects │ │ │ │
|
||||
│ │ • sla_contracts │ │ │ │
|
||||
│ │ • questionnaire_* │ │ │ │
|
||||
│ │ • blog_posts │ │ │ │
|
||||
│ │ • cases │ │ │ │
|
||||
│ │ • documents │ │ │ │
|
||||
│ │ • photos │ │ │ │
|
||||
│ │ • audit_log │ │ │ │
|
||||
│ └─────────────────────┘ └──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼──────────┐ │
|
||||
│ │ Redis │ (кэш, Celery broker) │
|
||||
│ └─────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Celery Workers (Python) │ │
|
||||
│ │ │ │
|
||||
│ │ • Генерация отчётов (PDF, Excel) │ │
|
||||
│ │ • Загрузка фото на Yandex Disk │ │
|
||||
│ │ • Интеграция с 1С (по мере необходимости) │ │
|
||||
│ │ • Фоновые расчёты, уведомления │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Gitea │ │ Certbot │ │
|
||||
│ │ (Git-сервер) │ │ (Let's Encrypt) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • aegisone-php │ │ • aegisone.ru │ │
|
||||
│ │ • aegisone-py │ │ • service.aeg.. │ │
|
||||
│ │ • project2 │ │ • other.ru │ │
|
||||
│ │ • CI/CD Actions │ │ • автообновление│ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Yandex Disk API (облачное хранилище) │ │
|
||||
│ │ │ │
|
||||
│ │ /AegisOne/ │ │
|
||||
│ │ ├── objects/{object_id}/ │ │
|
||||
│ │ │ ├── photos/{photo_id}.jpg │ │
|
||||
│ │ │ └── documents/{doc_id}/ │ │
|
||||
│ │ │ ├── {photo_id}.jpg │ │
|
||||
│ │ │ └── report_{doc_id}.pdf │ │
|
||||
│ │ └── ... │ │
|
||||
│ │ │ │
|
||||
│ │ В PostgreSQL хранятся: file_id, file_url, привязка к объекту│ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Технологический стек
|
||||
|
||||
### 3.1. Публичная часть (без изменений)
|
||||
| Компонент | Технология | Примечание |
|
||||
|---|---|---|
|
||||
| Язык | PHP 7.4 | Не обновлять, не трогать |
|
||||
| БД | MySQL | Мигрировать на PostgreSQL позже (опционально) |
|
||||
| Веб-сервер | Nginx + PHP-FPM | В Docker-контейнере |
|
||||
| CSS/JS | Inline | Без изменений |
|
||||
|
||||
### 3.2. Сервисная часть (Python)
|
||||
| Компонент | Технология | Примечание |
|
||||
|---|---|---|
|
||||
| Язык | Python 3.11+ | |
|
||||
| Фреймворк | FastAPI | REST API + серверный рендеринг |
|
||||
| Шаблоны | Jinja2 | Серверный рендеринг, как сейчас PHP |
|
||||
| Веб-сервер | Uvicorn | ASGI-сервер |
|
||||
| ORM | SQLAlchemy 2.0 + Alembic | Миграции БД |
|
||||
| БД | PostgreSQL 15+ | Единая БД для всех проектов |
|
||||
| Аутентификация | JWT (python-jose) | Токены для API и сессий |
|
||||
| Фоновые задачи | Celery + Redis | Отчёты, загрузка файлов, интеграции |
|
||||
| Генерация отчётов | WeasyPrint (PDF), openpyxl (Excel) | |
|
||||
| Yandex Disk | yandex-disk SDK / REST API | OAuth 2.0 |
|
||||
| Валидация | Pydantic | Схемы данных |
|
||||
|
||||
### 3.3. Инфраструктура
|
||||
| Компонент | Технология | Примечание |
|
||||
|---|---|---|
|
||||
| Контейнеризация | Docker + Docker Compose | |
|
||||
| Reverse proxy | Nginx | Маршрутизация по доменам |
|
||||
| SSL | Let's Encrypt + Certbot | Автообновление |
|
||||
| Git-сервер | Gitea | Лёгкий, встроенный CI/CD |
|
||||
| CI/CD | Gitea Actions | Автоматический деплой |
|
||||
| Мониторинг | (опционально) Uptime Kuma | |
|
||||
|
||||
### 3.4. Мобильные приложения (будущее)
|
||||
| Компонент | Технология | Примечание |
|
||||
|---|---|---|
|
||||
| Фреймворк | Flutter | |
|
||||
| API | FastAPI REST endpoints | Те же, что для веб-админки |
|
||||
| Аутентификация | JWT | |
|
||||
|
||||
---
|
||||
|
||||
## 4. Почему Jinja2, а не SPA
|
||||
|
||||
### 4.1. Сравнение
|
||||
|
||||
| Критерий | Jinja2 (SSR) | SPA (React/Vue) |
|
||||
|---|---|---|
|
||||
| Серверная нагрузка | Рендерит HTML на сервере | Отдаёт статику + JSON API |
|
||||
| Масштабирование | Горизонтальное: Nginx → N FastAPI инстансов | CDN для статики + Nginx → API |
|
||||
| Сложность разработки | Один стек, один деплой | Два стека, два деплоя, CORS |
|
||||
| Скорость разработки | Быстро | Медленнее |
|
||||
| Команда | 1-2 Python-разработчика | Python + JS/TS разработчики |
|
||||
| SEO | Не нужен (админка закрыта) | Не нужен |
|
||||
| Мобильные приложения | API тот же — Flutter подключается | API тот же — Flutter подключается |
|
||||
| Поддержка | Проще | Сложнее (два кодовых базы) |
|
||||
|
||||
### 4.2. Масштабирование Jinja2 по России
|
||||
|
||||
При росте нагрузки:
|
||||
1. **Вертикальное:** Увеличить ресурсы VPS (CPU, RAM)
|
||||
2. **Горизонтальное:** Несколько инстансов FastAPI за Nginx (load balancing)
|
||||
3. **Кэширование:** Redis для кэширования тяжёлых запросов
|
||||
4. **CDN:** Для статики (CSS, JS, изображения) — Cloudflare или аналог
|
||||
|
||||
Jinja2 **не является узким местом**. Узким местом будет БД или внешние API (Yandex Disk, 1С).
|
||||
|
||||
### 4.3. Вердикт
|
||||
|
||||
**Jinja2 — правильный выбор.** Для B2B-админки SPA — это оверинжиниринг. Jinja2 даёт:
|
||||
- Быструю разработку
|
||||
- Простую поддержку
|
||||
- Легкое масштабирование
|
||||
- Единую кодовую базу
|
||||
- Готовность к мобильным приложениям (тот же API)
|
||||
|
||||
---
|
||||
|
||||
## 5. Этапы реализации
|
||||
|
||||
### Этап 1: Docker-фундамент на VPS (1-2 недели)
|
||||
|
||||
**Цель:** Подготовить VPS для мультипроектной работы с Docker.
|
||||
|
||||
#### 1.1. Установка Docker и Docker Compose
|
||||
```bash
|
||||
# На VPS (Ubuntu/Debian)
|
||||
apt update && apt upgrade -y
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker
|
||||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
#### 1.2. Базовая структура директорий на VPS
|
||||
```
|
||||
/opt/
|
||||
├── nginx-proxy/ # Nginx reverse proxy + Certbot
|
||||
│ ├── docker-compose.yml
|
||||
│ ├── nginx.conf
|
||||
│ └── certs/ # Let's Encrypt сертификаты
|
||||
├── gitea/ # Git-сервер
|
||||
│ ├── docker-compose.yml
|
||||
│ └── data/ # Данные Gitea
|
||||
├── aegisone-php/ # Публичная часть (PHP)
|
||||
│ ├── docker-compose.yml
|
||||
│ └── src/ # Исходный код (git clone)
|
||||
├── aegisone-py/ # Сервисная часть (Python)
|
||||
│ ├── docker-compose.yml
|
||||
│ ├── app/ # FastAPI приложение
|
||||
│ ├── migrations/ # Alembic миграции
|
||||
│ └── requirements.txt
|
||||
└── project2/ # Существующий проект
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
#### 1.3. Nginx reverse proxy + Certbot
|
||||
- Один Nginx-контейнер на портах 80/443
|
||||
- Маршрутизация по `server_name`:
|
||||
- `aegisone.ru` → `aegisone-php:80`
|
||||
- `service.aegisone.ru` → `aegisone-py:8000`
|
||||
- `git.aegisone.ru` → `gitea:3000`
|
||||
- `other-project.ru` → `project2:8000`
|
||||
- Certbot в отдельном контейнере или встроенный (nginx-proxy + acme-companion)
|
||||
|
||||
#### 1.4. Gitea (Git-сервер)
|
||||
- Docker-образ `gitea/gitea:latest`
|
||||
- Порт 3000 → `git.aegisone.ru`
|
||||
- PostgreSQL для данных Gitea
|
||||
- Встроенный CI/CD (Gitea Actions)
|
||||
- Репозитории:
|
||||
- `aegisone-php` (публичная часть, перенос с текущего хостинга)
|
||||
- `aegisone-py` (новая сервисная часть)
|
||||
- `project2` (существующий проект)
|
||||
- `infra` (конфигурации Docker, Nginx, CI/CD)
|
||||
|
||||
#### 1.5. Обёртка существующего проекта в Docker
|
||||
- Создать `docker-compose.yml` для project2
|
||||
- Протестировать работоспособность
|
||||
- Настроить домен `other-project.ru`
|
||||
|
||||
**Критерий завершения этапа:**
|
||||
- [ ] Docker + Docker Compose установлены
|
||||
- [ ] Nginx reverse proxy работает, маршрутизирует по доменам
|
||||
- [ ] Certbot выдаёт SSL-сертификаты (Let's Encrypt)
|
||||
- [ ] Gitea запущен на `git.aegisone.ru`
|
||||
- [ ] Существующий проект работает в Docker
|
||||
- [ ] Все домены доступны по HTTPS
|
||||
|
||||
---
|
||||
|
||||
### Этап 2: Перенос AegisOne на VPS (1 неделя)
|
||||
|
||||
**Цель:** Перенести публичную часть с хостинга на VPS, отключить хостинг.
|
||||
|
||||
#### 2.1. Docker-контейнер для PHP
|
||||
```yaml
|
||||
# /opt/aegisone-php/docker-compose.yml
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "9080:80" # Внутренний порт, внешний через reverse proxy
|
||||
volumes:
|
||||
- ./src:/var/www/html
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- php
|
||||
|
||||
php:
|
||||
image: php:7.4-fpm
|
||||
volumes:
|
||||
- ./src:/var/www/html
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_NAME=aegisone
|
||||
- DB_USER=aegisone
|
||||
- DB_PASS=<password>
|
||||
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: <root_password>
|
||||
MYSQL_DATABASE: aegisone
|
||||
MYSQL_USER: aegisone
|
||||
MYSQL_PASSWORD: <password>
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
```
|
||||
|
||||
#### 2.2. Перенос файлов
|
||||
- Скачать все файлы с хостинга (FTP/SFTP)
|
||||
- Загрузить в `/opt/aegisone-php/src/`
|
||||
- Настроить `config.php` для нового окружения (DB_HOST=mysql)
|
||||
|
||||
#### 2.3. Миграция БД
|
||||
- Экспорт MySQL с хостинга: `mysqldump -u user -p db_name > backup.sql`
|
||||
- Импорт в Docker MySQL: `docker exec -i aegisone-php-mysql mysql -u aegisone -p aegisone < backup.sql`
|
||||
- Проверить работоспособность
|
||||
|
||||
#### 2.4. Настройка домена
|
||||
- DNS: `aegisone.ru` → IP VPS
|
||||
- Nginx reverse proxy: `server_name aegisone.ru` → `aegisone-php:9080`
|
||||
- SSL: Certbot для `aegisone.ru`
|
||||
|
||||
#### 2.5. Тестирование и отключение хостинга
|
||||
- Проверить все страницы: главная, услуги, блог, контакты, форма
|
||||
- Проверить карусель, FAQ, тему (светлая/тёмная)
|
||||
- Проверить Яндекс.Карту с переключением темы
|
||||
- После подтверждения — отключить хостинг
|
||||
|
||||
**Критерий завершения этапа:**
|
||||
- [ ] Публичная часть работает на VPS
|
||||
- [ ] Все страницы загружаются корректно
|
||||
- [ ] Форма обратной связи работает
|
||||
- [ ] Яндекс.Карта переключает тему
|
||||
- [ ] Домен `aegisone.ru` указывает на VPS
|
||||
- [ ] Хостинг отключён
|
||||
|
||||
---
|
||||
|
||||
### Этап 3: Python-админка (FastAPI + Jinja2) (3-5 недель)
|
||||
|
||||
**Цель:** Полностью переписать сервисную часть на Python.
|
||||
|
||||
#### 3.1. Структура проекта
|
||||
```
|
||||
aegisone-py/
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── alembic.ini
|
||||
├── migrations/ # Alembic миграции
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI приложение
|
||||
│ ├── config.py # Настройки (env vars)
|
||||
│ ├── database.py # SQLAlchemy engine, session
|
||||
│ ├── models/ # SQLAlchemy модели
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── user.py
|
||||
│ │ ├── customer.py
|
||||
│ │ ├── object.py
|
||||
│ │ ├── sla.py
|
||||
│ │ ├── questionnaire.py
|
||||
│ │ ├── blog.py
|
||||
│ │ ├── case.py
|
||||
│ │ ├── document.py
|
||||
│ │ ├── photo.py
|
||||
│ │ └── audit_log.py
|
||||
│ ├── schemas/ # Pydantic схемы
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── user.py
|
||||
│ │ ├── customer.py
|
||||
│ │ └── ...
|
||||
│ ├── api/ # API endpoints
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── auth.py # Логин, JWT
|
||||
│ │ ├── users.py
|
||||
│ │ ├── customers.py
|
||||
│ │ ├── objects.py
|
||||
│ │ ├── sla.py
|
||||
│ │ ├── questionnaire.py
|
||||
│ │ ├── blog.py
|
||||
│ │ ├── cases.py
|
||||
│ │ ├── documents.py
|
||||
│ │ └── reports.py
|
||||
│ ├── templates/ # Jinja2 шаблоны
|
||||
│ │ ├── base.html # Базовый шаблон
|
||||
│ │ ├── login.html
|
||||
│ │ ├── dashboard.html
|
||||
│ │ ├── users/
|
||||
│ │ ├── customers/
|
||||
│ │ ├── objects/
|
||||
│ │ ├── sla/
|
||||
│ │ ├── questionnaire/
|
||||
│ │ ├── blog/
|
||||
│ │ ├── documents/
|
||||
│ │ └── reports/
|
||||
│ ├── static/ # CSS, JS, изображения
|
||||
│ │ ├── css/
|
||||
│ │ │ ├── service.css # Стили админки
|
||||
│ │ │ └── ...
|
||||
│ │ ├── js/
|
||||
│ │ │ ├── main.js
|
||||
│ │ │ └── ...
|
||||
│ │ └── img/
|
||||
│ ├── services/ # Бизнес-логика
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── auth_service.py
|
||||
│ │ ├── calc_service.py # Расчёты (Risk Score, Object Index, SLA)
|
||||
│ │ ├── report_service.py # Генерация отчётов
|
||||
│ │ ├── yandex_disk_service.py # Интеграция с Yandex Disk
|
||||
│ │ └── ...
|
||||
│ ├── tasks/ # Celery задачи
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── reports.py
|
||||
│ │ ├── yandex_disk.py
|
||||
│ │ └── ...
|
||||
│ └── utils/ # Утилиты
|
||||
│ ├── __init__.py
|
||||
│ ├── password.py
|
||||
│ └── ...
|
||||
└── tests/ # Тесты (опционально)
|
||||
```
|
||||
|
||||
#### 3.2. Миграция данных из MySQL в PostgreSQL
|
||||
- Создать Alembic миграции для всех таблиц
|
||||
- Написать скрипт миграции данных:
|
||||
- `users` → `users`
|
||||
- `customers` → `customers`
|
||||
- `objects` → `objects`
|
||||
- `sla_contracts` → `sla_contracts`
|
||||
- `questionnaire_sessions`, `questionnaire_answers`, `questionnaire_items` → аналогично
|
||||
- `blog_posts` → `blog_posts`
|
||||
- `cases` → `cases`
|
||||
- `audit_log` → `audit_log`
|
||||
- Протестировать миграцию на копии БД
|
||||
|
||||
#### 3.3. Аутентификация
|
||||
- JWT-токены (python-jose)
|
||||
- Логин/пароль (bcrypt)
|
||||
- Роли: `owner`, `engineer`, `technician`
|
||||
- Сессии через cookies (для Jinja2) + JWT (для API)
|
||||
- CSRF-защита для форм
|
||||
|
||||
#### 3.4. CRUD-модули (по порядку приоритета)
|
||||
|
||||
**3.4.1. Пользователи (`/service/users/`)**
|
||||
- Список, создание, редактирование, удаление
|
||||
- Роль `owner` нельзя удалить, нельзя изменить роль
|
||||
- Блокировка/разблокировка
|
||||
- Аудит действий
|
||||
|
||||
**3.4.2. Клиенты (`/service/customers/`)**
|
||||
- Список, создание, редактирование, удаление
|
||||
- ИНН, КПП, юридический адрес, контакты
|
||||
- Связь с объектами и SLA
|
||||
|
||||
**3.4.3. Объекты (`/service/objects/`)**
|
||||
- Список, создание, редактирование, удаление
|
||||
- Привязка к клиенту
|
||||
- Тип, адрес, площадь, сотрудники
|
||||
- Risk Score, Complexity Index, Object Index (авторасчёт)
|
||||
- SLA price (авторасчёт)
|
||||
|
||||
**3.4.4. SLA контракты (`/service/sla/`)**
|
||||
- Список, создание, редактирование, удаление
|
||||
- Привязка к клиенту и объекту
|
||||
- Тарифы: Базовый, Оптимальный, Максимальный
|
||||
- Время реакции, периодичность ТО
|
||||
- Статус: active, expired, cancelled, negotiation
|
||||
|
||||
**3.4.5. Опросник (`/service/questionnaire/`)**
|
||||
- Создание новой сессии
|
||||
- 5 шагов: Коммерческий, Технический, Эксплуатация, Риски, Расчёт SLA
|
||||
- Вопросы из БД (`questionnaire_items`)
|
||||
- Сохранение ответов
|
||||
- Авторасчёт: Risk Score, Complexity Index, Infrastructure Load, Service History, Object Index, SLA Price
|
||||
- Формирование паспорта объекта
|
||||
|
||||
**3.4.6. Блог (`/service/blog/`)**
|
||||
- Список, создание, редактирование, удаление
|
||||
- Категории: audit, sla, incident, supervision, documentation, risk, cases
|
||||
- HTML-редактор (toolbar)
|
||||
- Автогенерация slug из заголовка
|
||||
- Статус: draft, published
|
||||
|
||||
**3.4.7. Примеры из практики (`/service/cases/`)**
|
||||
- Список, создание, редактирование, удаление
|
||||
- Заголовок, текст, эффект
|
||||
- Сортировка, активность
|
||||
|
||||
**3.4.8. Документы (`/service/documents/`)**
|
||||
- Список документов (из `service/docs/*.md`)
|
||||
- Ручная сортировка
|
||||
- Переименование
|
||||
- Гранулярные права: view, edit, cancel (по ролям)
|
||||
- JSON-хранилище прав (`docs_permissions.json`)
|
||||
|
||||
#### 3.5. Расчёты (перенос из PHP)
|
||||
- `calc_risk_score()` — Risk Score
|
||||
- `calc_complexity()` — Complexity Index
|
||||
- `calc_infrastructure_load()` — Infrastructure Load
|
||||
- `calc_service_history()` — Service History
|
||||
- `calc_object_index()` — Object Index
|
||||
- `calc_sla_price()` — SLA Price
|
||||
- `risk_multiplier()` — множитель риска
|
||||
- `risk_label()` — текстовая метка риска
|
||||
- `object_class()` — класс объекта
|
||||
- `fmt_money()` — форматирование денег
|
||||
|
||||
Все функции переписать на Python, покрыть тестами.
|
||||
|
||||
#### 3.6. Дашборд (`/service/dashboard/`)
|
||||
- Метрики: количество объектов, активных SLA, открытых задач
|
||||
- Графики (Chart.js или аналог)
|
||||
- Быстрые действия
|
||||
- Последние активности (audit_log)
|
||||
|
||||
#### 3.7. Фронтенд (Jinja2)
|
||||
- Базовый шаблон (`base.html`) с:
|
||||
- Боковой панелью (sidebar) с навигацией
|
||||
- Верхней панелью (header) с пользователем и переключателем темы
|
||||
- Основным контентом
|
||||
- Темы: system → dark → light (localStorage, как сейчас)
|
||||
- Кастомные скроллбары
|
||||
- Тултипы на кнопках
|
||||
- Модальные окна (создание/редактирование)
|
||||
- Таблицы с поиском и пагинацией
|
||||
- Адаптивность (мобильная версия)
|
||||
|
||||
**Критерий завершения этапа:**
|
||||
- [ ] Все CRUD-модули работают
|
||||
- [ ] Расчёты корректны (сравнить с PHP-версией)
|
||||
- [ ] Миграция данных завершена
|
||||
- [ ] Фронтенд полностью функционален
|
||||
- [ ] Темы переключаются
|
||||
- [ ] Мобильная версия работает
|
||||
- [ ] `service.aegisone.ru` доступен по HTTPS
|
||||
|
||||
---
|
||||
|
||||
### Этап 4: Интеграции (2-3 недели)
|
||||
|
||||
**Цель:** Добавить генерацию отчётов, интеграцию с Yandex Disk, фоновые задачи.
|
||||
|
||||
#### 4.1. Yandex Disk API
|
||||
- Регистрация приложения в Yandex OAuth
|
||||
- Получение OAuth-токена
|
||||
- SDK: `yandex-disk` или REST API через `requests`
|
||||
- Структура папок:
|
||||
```
|
||||
/AegisOne/
|
||||
├── objects/{object_id}/
|
||||
│ ├── photos/
|
||||
│ │ └── {photo_id}.jpg
|
||||
│ └── documents/{doc_id}/
|
||||
│ ├── {photo_id}.jpg
|
||||
│ └── report_{doc_id}.pdf
|
||||
└── ...
|
||||
```
|
||||
- Эндпоинты:
|
||||
- `POST /api/documents/{id}/upload-photo` — загрузка фото
|
||||
- `GET /api/documents/{id}/photos` — список фото
|
||||
- `DELETE /api/photos/{id}` — удаление фото
|
||||
- В БД: таблица `photos` (id, document_id, object_id, yandex_file_id, yandex_file_url, uploaded_at, uploaded_by)
|
||||
|
||||
#### 4.2. Генерация отчётов
|
||||
- **PDF:** WeasyPrint (HTML → PDF)
|
||||
- Шаблон паспорта объекта
|
||||
- Шаблон отчёта по аудиту
|
||||
- Шаблон SLA-контракта
|
||||
- **Excel:** openpyxl
|
||||
- Экспорт списка объектов
|
||||
- Экспорт SLA-контрактов
|
||||
- Экспорт отчётов по KPI
|
||||
- Фоновая генерация через Celery (для тяжёлых отчётов)
|
||||
|
||||
#### 4.3. Celery (фоновые задачи)
|
||||
- Broker: Redis
|
||||
- Задачи:
|
||||
- Генерация PDF-отчётов
|
||||
- Загрузка фото на Yandex Disk
|
||||
- Отправка уведомлений (email, Telegram)
|
||||
- Интеграция с 1С (по мере необходимости)
|
||||
- Мониторинг: Flower (веб-интерфейс для Celery)
|
||||
|
||||
#### 4.4. Интеграция с 1С (опционально, по мере необходимости)
|
||||
- REST API 1С или OData
|
||||
- Синхронизация: клиенты, объекты, договоры
|
||||
- Фоновая задача Celery
|
||||
|
||||
**Критерий завершения этапа:**
|
||||
- [ ] Yandex Disk API интегрирован
|
||||
- [ ] Фото загружаются и привязываются к документам
|
||||
- [ ] PDF-отчёты генерируются
|
||||
- [ ] Excel-экспорт работает
|
||||
- [ ] Celery запущен, задачи выполняются
|
||||
- [ ] (Опционально) Интеграция с 1С настроена
|
||||
|
||||
---
|
||||
|
||||
### Этап 5: CI/CD и автоматизация деплоя (1 неделя)
|
||||
|
||||
**Цель:** Настроить автоматический деплой при push в Git.
|
||||
|
||||
#### 5.1. Gitea Actions (CI/CD)
|
||||
- Встроенный CI/CD в Gitea (совместим с GitHub Actions)
|
||||
- Workflow для `aegisone-py`:
|
||||
```yaml
|
||||
name: Deploy AegisOne Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
docker build -t registry.git.aegisone.ru/aegisone-py:latest .
|
||||
docker push registry.git.aegisone.ru/aegisone-py:latest
|
||||
|
||||
- name: Deploy to VPS
|
||||
run: |
|
||||
ssh user@vps "cd /opt/aegisone-py && docker compose pull && docker compose up -d"
|
||||
```
|
||||
|
||||
#### 5.2. Структура репозиториев в Gitea
|
||||
```
|
||||
git.aegisone.ru/
|
||||
├── aegisone/aegisone-php # Публичная часть (PHP)
|
||||
├── aegisone/aegisone-py # Сервисная часть (Python)
|
||||
├── aegisone/infra # Docker, Nginx, CI/CD конфиги
|
||||
└── aegisone/project2 # Существующий проект
|
||||
```
|
||||
|
||||
#### 5.3. Автоматический деплой
|
||||
- Push в `main` → Gitea Actions → сборка Docker-образа → деплой на VPS
|
||||
- Rollback: `docker compose down && docker compose up -d` с предыдущим образом
|
||||
- Миграции БД: Alembic `upgrade head` автоматически при деплое
|
||||
|
||||
#### 5.4. Локальная разработка
|
||||
- `docker-compose.dev.yml` для локальной разработки
|
||||
- Hot-reload для Python (uvicorn --reload)
|
||||
- Локальная БД (PostgreSQL в Docker)
|
||||
- Синхронизация с VPS: `git push` → автоматический деплой
|
||||
|
||||
**Критерий завершения этапа:**
|
||||
- [ ] Gitea Actions настроены для всех проектов
|
||||
- [ ] Push в `main` → автоматический деплой
|
||||
- [ ] Rollback работает
|
||||
- [ ] Миграции БД применяются автоматически
|
||||
- [ ] Локальная разработка через Docker
|
||||
|
||||
---
|
||||
|
||||
## 6. Миграция данных: MySQL → PostgreSQL
|
||||
|
||||
### 6.1. Таблицы для миграции
|
||||
|
||||
| MySQL таблица | PostgreSQL таблица | Примечание |
|
||||
|---|---|---|
|
||||
| `users` | `users` | Без изменений |
|
||||
| `login_attempts` | `login_attempts` | Без изменений |
|
||||
| `audit_log` | `audit_log` | Без изменений |
|
||||
| `objects` | `objects` | + `customer_id` FK |
|
||||
| `object_assignments` | `object_assignments` | Без изменений |
|
||||
| `tasks` | `tasks` | Без изменений |
|
||||
| `task_comments` | `task_comments` | Без изменений |
|
||||
| `customers` | `customers` | Новая таблица |
|
||||
| `sla_contracts` | `sla_contracts` | + `customer_id` FK, `contract_number`, `description`, `response_time_hours` |
|
||||
| `incidents` | `incidents` | Без изменений |
|
||||
| `engineer_kpi` | `engineer_kpi` | Без изменений |
|
||||
| `questionnaire_sessions` | `questionnaire_sessions` | Без изменений |
|
||||
| `questionnaire_answers` | `questionnaire_answers` | Без изменений |
|
||||
| `questionnaire_items` | `questionnaire_items` | Новая таблица |
|
||||
| `object_passports` | `object_passports` | Без изменений |
|
||||
| `blog_posts` | `blog_posts` | ENUM категории изменён |
|
||||
| `cases` | `cases` | Без изменений |
|
||||
|
||||
### 6.2. Скрипт миграции
|
||||
```python
|
||||
# scripts/migrate_mysql_to_postgres.py
|
||||
import mysql.connector
|
||||
import psycopg2
|
||||
from psycopg2.extras import execute_batch
|
||||
|
||||
# Подключение к MySQL
|
||||
mysql_conn = mysql.connector.connect(host='...', user='...', password='...', database='...')
|
||||
mysql_cursor = mysql_conn.cursor(dictionary=True)
|
||||
|
||||
# Подключение к PostgreSQL
|
||||
pg_conn = psycopg2.connect(host='...', user='...', password='...', database='...')
|
||||
pg_cursor = pg_conn.cursor()
|
||||
|
||||
# Миграция каждой таблицы
|
||||
tables = ['users', 'customers', 'objects', 'sla_contracts', ...]
|
||||
for table in tables:
|
||||
mysql_cursor.execute(f"SELECT * FROM {table}")
|
||||
rows = mysql_cursor.fetchall()
|
||||
|
||||
if rows:
|
||||
columns = rows[0].keys()
|
||||
placeholders = ', '.join(['%s'] * len(columns))
|
||||
cols = ', '.join(columns)
|
||||
execute_batch(
|
||||
pg_cursor,
|
||||
f"INSERT INTO {table} ({cols}) VALUES ({placeholders}) ON CONFLICT DO NOTHING",
|
||||
[tuple(row[col] for col in columns) for row in rows]
|
||||
)
|
||||
|
||||
pg_conn.commit()
|
||||
```
|
||||
|
||||
### 6.3. Проверка миграции
|
||||
- Сравнить количество записей в MySQL и PostgreSQL
|
||||
- Проверить FK-связи
|
||||
- Протестировать CRUD-операции на PostgreSQL
|
||||
- Откат: сохранить бэкап MySQL до миграции
|
||||
|
||||
---
|
||||
|
||||
## 7. Безопасность
|
||||
|
||||
### 7.1. Аутентификация и авторизация
|
||||
- JWT-токены с expiration (1 час)
|
||||
- Refresh tokens (7 дней)
|
||||
- Роли: `owner`, `engineer`, `technician`
|
||||
- Гранулярные права для документов (view, edit, cancel)
|
||||
- CSRF-защита для форм
|
||||
|
||||
### 7.2. Защита данных
|
||||
- Пароли: bcrypt (cost factor 12)
|
||||
- HTTPS для всех доменов (Let's Encrypt)
|
||||
- Firewall: только порты 80, 443, 22 (SSH)
|
||||
- Docker: изоляция контейнеров, нет root в контейнерах
|
||||
- БД: нет внешнего доступа, только из Docker-сети
|
||||
|
||||
### 7.3. Бэкапы
|
||||
- PostgreSQL: `pg_dump` ежедневно (cron)
|
||||
- Yandex Disk: файлы уже в облаке
|
||||
- Gitea: бэкап репозиториев (tar)
|
||||
- Хранение бэкапов: отдельный диск или облако
|
||||
|
||||
### 7.4. Мониторинг
|
||||
- (Опционально) Uptime Kuma для мониторинга доступности
|
||||
- Логи: Docker logs + ротация
|
||||
- Алерты: email/Telegram при ошибках
|
||||
|
||||
---
|
||||
|
||||
## 8. Риски и митигация
|
||||
|
||||
| Риск | Вероятность | Влияние | Митигация |
|
||||
|---|---|---|---|
|
||||
| Ошибка миграции данных | Средняя | Высокое | Тестирование на копии, бэкап MySQL |
|
||||
| Простои при деплое | Низкая | Среднее | Zero-downtime деплой (docker compose up -d) |
|
||||
| Проблемы с SSL | Низкая | Среднее | Certbot автообновление, мониторинг |
|
||||
| Yandex Disk API лимиты | Низкая | Низкое | Кэширование, retry logic |
|
||||
| Нехватка ресурсов VPS | Средняя | Высокое | Мониторинг CPU/RAM, масштабирование |
|
||||
| Ошибки в расчётах | Средняя | Высокое | Тесты, сравнение с PHP-версией |
|
||||
|
||||
---
|
||||
|
||||
## 9. Оценки времени
|
||||
|
||||
| Этап | Описание | Оценка |
|
||||
|---|---|---|
|
||||
| Этап 1 | Docker-фундамент | 1-2 недели |
|
||||
| Этап 2 | Перенос AegisOne на VPS | 1 неделя |
|
||||
| Этап 3 | Python-админка | 3-5 недель |
|
||||
| Этап 4 | Интеграции | 2-3 недели |
|
||||
| Этап 5 | CI/CD | 1 неделя |
|
||||
| **Итого** | | **8-12 недель** |
|
||||
|
||||
---
|
||||
|
||||
## 10. Контрольные точки
|
||||
|
||||
| Точка | Описание | Критерий успеха |
|
||||
|---|---|---|
|
||||
| КП1 | Docker-фундамент готов | Все сервисы запущены, домены работают по HTTPS |
|
||||
| КП2 | Публичная часть на VPS | aegisone.ru работает, хостинг отключён |
|
||||
| КП3 | Python-админка MVP | CRUD для пользователей, объектов, SLA работает |
|
||||
| КП4 | Python-админка полная | Все модули, расчёты, миграция данных завершены |
|
||||
| КП5 | Интеграции | Yandex Disk, отчёты, Celery работают |
|
||||
| КП6 | CI/CD | Автоматический деплой при push |
|
||||
|
||||
---
|
||||
|
||||
## 11. Открытые вопросы (уточнить перед реализацией)
|
||||
|
||||
> **Важно:** Если при реализации возникнут вопросы — лучше уточнить, чем переделывать потом.
|
||||
|
||||
1. **Миграция данных:** Переносим все данные из MySQL в PostgreSQL или начинаем с чистой БД? (Рекомендация: мигрировать)
|
||||
2. **Домен для админки:** `service.aegisone.ru` или `app.aegisone.ru`? (Рекомендация: `service.aegisone.ru`)
|
||||
3. **Yandex Disk OAuth:** Использовать токен приложения или OAuth с авторизацией пользователя? (Рекомендация: токен приложения для простоты)
|
||||
4. **Генерация отчётов:** Какие именно отчёты нужны в первую очередь? (Паспорт объекта, отчёт по аудиту, SLA-контракт?)
|
||||
5. **Интеграция с 1С:** Какая версия 1С? Какой метод интеграции (REST API, OData, файловый обмен)?
|
||||
6. **Мониторинг:** Нужен ли Uptime Kuma или другой мониторинг? (Рекомендация: да, для продакшена)
|
||||
7. **Бэкапы:** Где хранить бэкапы PostgreSQL? (Отдельный диск на VPS, облако, другой сервер?)
|
||||
8. **Тестирование:** Покрывать ли код тестами? (Рекомендация: да, хотя бы критические расчёты)
|
||||
9. **Документация:** Вести ли документацию по API? (Рекомендация: да, через FastAPI автоматическую /docs)
|
||||
10. **Логирование:** Какой уровень логирования? Куда писать логи? (Рекомендация: INFO в stdout, Docker logs)
|
||||
|
||||
---
|
||||
|
||||
## 12. Следующие шаги
|
||||
|
||||
1. **Утвердить план** — ответить на открытые вопросы (раздел 11)
|
||||
2. **Начать Этап 1** — Docker-фундамент на VPS
|
||||
3. **Параллельно** — создать репозитории в Gitea, настроить CI/CD skeleton
|
||||
4. **После Этапа 1** — приступить к Этапу 2 (перенос PHP)
|
||||
5. **После Этапа 2** — приступить к Этапу 3 (Python-админка)
|
||||
|
||||
---
|
||||
|
||||
**Документ создан:** 2026-05-17
|
||||
**Версия:** 1.0
|
||||
**Автор:** AI-ассистент + Владелец
|
||||
**Статус:** Готов к реализации после утверждения
|
||||
@@ -0,0 +1,485 @@
|
||||
Ниже — система KPI, которая превращает инженеров и техников из “выездных исполнителей” в управляемую часть инженерной SLA-модели AegisOne Engineering.
|
||||
|
||||
Главная цель системы:
|
||||
|
||||
> не “оценивать занятость”, а измерять качество эксплуатации, скорость реакции и снижение рисков объекта
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
0. ПРИНЦИП СИСТЕМЫ KPI
|
||||
|
||||
Ты НЕ измеряешь:
|
||||
|
||||
сколько выездов сделал инженер
|
||||
|
||||
сколько он “починил”
|
||||
|
||||
|
||||
Ты измеряешь:
|
||||
|
||||
1) надежность объектов
|
||||
|
||||
2) соблюдение SLA
|
||||
|
||||
3) качество диагностики
|
||||
|
||||
4) снижение повторных инцидентов
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. СТРУКТУРА KPI (3 УРОВНЯ)
|
||||
|
||||
2. Операционный KPI (ежедневный)
|
||||
3. SLA KPI (контрактный)
|
||||
4. Инженерный KPI качества (экспертный)
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. ОПЕРАЦИОННЫЙ KPI (Execution KPI)
|
||||
|
||||
Это “что сделал инженер”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 1: Время реакции (Response Time)
|
||||
|
||||
Формула:
|
||||
|
||||
RT = (фактическое время реакции / SLA время реакции)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Оценка:
|
||||
|
||||
RT Оценка
|
||||
|
||||
≤ 1.0 норма
|
||||
1.0–1.2 допустимо
|
||||
> 1.2 нарушение
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
SLA: 2 часа
|
||||
|
||||
факт: 3 часа
|
||||
|
||||
|
||||
RT = 3 / 2 = 1.5 → ❌ нарушение
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 2: Время устранения (Resolution Time)
|
||||
|
||||
TTR = фактическое время устранения / нормативное время
|
||||
|
||||
|
||||
---
|
||||
|
||||
Важно:
|
||||
|
||||
Норматив зависит от типа инцидента:
|
||||
|
||||
P1 (критический) — 2–6 часов
|
||||
|
||||
P2 — до 24 часов
|
||||
|
||||
P3 — до 3 дней
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 3: Закрытие заявок в SLA
|
||||
|
||||
SLA Compliance = (заявки в SLA / все заявки) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
≥ 95% — отлично
|
||||
|
||||
90–95% — допустимо
|
||||
|
||||
< 90% — проблема инженера
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 4: Повторные обращения (Reopen Rate)
|
||||
|
||||
RR = (повторные заявки / общее число заявок) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
≤ 5% — хорошо
|
||||
|
||||
5–10% — средне
|
||||
|
||||
> 10% — плохая диагностика
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. SLA KPI (контрактный уровень)
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 5: Выполнение SLA по объектам
|
||||
|
||||
Object SLA = (объекты без нарушений SLA / все объекты) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
≥ 95% — стабильная сеть объектов
|
||||
|
||||
< 90% — системная проблема команды
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 6: Доступность систем (System Uptime)
|
||||
|
||||
Uptime = (время работы системы / общее время) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Цель:
|
||||
|
||||
99%+ для критических объектов
|
||||
|
||||
97–99% допустимо
|
||||
|
||||
< 97% — риск SLA разрыва
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ИНЖЕНЕРНЫЙ KPI (КАЧЕСТВО ЭКСПЕРТИЗЫ)
|
||||
|
||||
Это ключевой уровень, который отличает тебя от монтажников.
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 7: Точность диагностики
|
||||
|
||||
Accuracy = (подтвержденные неисправности / выявленные) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
выявил 10 проблем
|
||||
|
||||
9 подтвердились
|
||||
|
||||
|
||||
→ 90%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
≥ 85% — хорошо
|
||||
|
||||
< 80% — инженер слабый
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 8: Глубина диагностики (Risk Coverage Score)
|
||||
|
||||
Оценивает, насколько инженер видит системные риски.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула (балльная система):
|
||||
|
||||
Каждый объект оценивается по 5 зонам:
|
||||
|
||||
видео
|
||||
|
||||
СКУД
|
||||
|
||||
пожарка
|
||||
|
||||
питание
|
||||
|
||||
сеть
|
||||
|
||||
|
||||
RCS = (проверенные зоны / 5) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
100% — стандарт AegisOne
|
||||
|
||||
< 80% — поверхностная работа
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 9: Снижение повторных инцидентов
|
||||
|
||||
IRR = ((инциденты до - инциденты после) / до) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Это ключевой KPI бизнеса
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. ФИНАНСОВЫЙ KPI ИНЖЕНЕРА
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 10: Доход на инженера (Revenue per Engineer)
|
||||
|
||||
RPE = общий SLA доход / количество инженеров
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
500k – слабая загрузка
|
||||
|
||||
800k – норм
|
||||
|
||||
1.2M+ – сильная модель
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. ИТОГОВЫЙ ИНДЕКС ИНЖЕНЕРА (ENGINEER SCORE)
|
||||
|
||||
Это твой главный инструмент управления.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
ES =
|
||||
(0.25 × SLA Compliance) +
|
||||
(0.20 × Response Time Score) +
|
||||
(0.20 × Resolution Time Score) +
|
||||
(0.15 × Diagnosis Accuracy) +
|
||||
(0.10 × Reopen Rate Score) +
|
||||
(0.10 × Risk Coverage Score)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Интерпретация:
|
||||
|
||||
Score Уровень
|
||||
|
||||
90–100 Senior Engineer
|
||||
80–89 Strong Engineer
|
||||
70–79 Middle
|
||||
< 70 требует обучения
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. ДОПОЛНИТЕЛЬНЫЕ KPI (ОЧЕНЬ ВАЖНЫЕ)
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 11: Документирование (Documentation Quality)
|
||||
|
||||
DQ = (полные отчёты / все выезды) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 12: Качество отчёта (Report Quality Score)
|
||||
|
||||
Оценивается по чек-листу:
|
||||
|
||||
есть ли риски
|
||||
|
||||
есть ли вывод
|
||||
|
||||
есть ли рекомендации
|
||||
|
||||
есть ли фото/фиксация
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI 13: Самостоятельность инженера
|
||||
|
||||
Autonomy = (самостоятельные решения / всего решений) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. КАК ЭТО ВЫГЛЯДИТ В CRM
|
||||
|
||||
|
||||
---
|
||||
|
||||
Инженер:
|
||||
- SLA Compliance: 96%
|
||||
- RT: 1.1
|
||||
- TTR: 0.9
|
||||
- Diagnosis: 88%
|
||||
- Reopen: 3%
|
||||
- Score: 91 (Senior)
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. СИСТЕМА МОТИВАЦИИ (ВАЖНО)
|
||||
|
||||
|
||||
---
|
||||
|
||||
БАЗОВАЯ ЛОГИКА:
|
||||
|
||||
фикс оклад
|
||||
|
||||
бонус за KPI
|
||||
|
||||
штрафы только за SLA нарушения
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
БОНУС:
|
||||
|
||||
Score 90+ → +20% к премии
|
||||
Score 80–89 → +10%
|
||||
Score < 80 → без бонуса
|
||||
|
||||
|
||||
---
|
||||
|
||||
10. ГЛАВНЫЙ ПРИНЦИП СИСТЕМЫ
|
||||
|
||||
Ты НЕ платишь за:
|
||||
|
||||
❌ “работу”
|
||||
❌ “выезды”
|
||||
|
||||
Ты платишь за:
|
||||
|
||||
стабильность инфраструктуры клиента
|
||||
|
||||
|
||||
---
|
||||
|
||||
11. КАК ЭТА СИСТЕМА МАСШТАБИРУЕТ БИЗНЕС
|
||||
|
||||
|
||||
---
|
||||
|
||||
1 инженер = управляемая единица SLA
|
||||
|
||||
Ты можешь:
|
||||
|
||||
добавлять инженеров
|
||||
|
||||
сравнивать эффективность
|
||||
|
||||
масштабировать регионы
|
||||
|
||||
контролировать качество без присутствия
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
12. СВЯЗЬ С ТВОЕЙ БИЗНЕС-МОДЕЛЬЮ
|
||||
|
||||
|
||||
---
|
||||
|
||||
KPI инженера
|
||||
↓
|
||||
качество SLA
|
||||
↓
|
||||
удержание клиентов
|
||||
↓
|
||||
MRR рост
|
||||
↓
|
||||
масштаб компании
|
||||
|
||||
|
||||
---
|
||||
|
||||
13. СЛЕДУЮЩИЙ УРОВЕНЬ (если продолжать систему)
|
||||
|
||||
Я могу дальше собрать:
|
||||
|
||||
систему грейдов инженеров (Junior → Lead → Chief)
|
||||
|
||||
модель расчёта зарплаты под KPI
|
||||
|
||||
автоматическую таблицу KPI в Excel/Notion
|
||||
|
||||
SLA dashboard (как у IT-компаний)
|
||||
|
||||
систему контроля качества через аудит отчётов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ИТОГ
|
||||
|
||||
Ты получил не “мотивацию сотрудников”.
|
||||
|
||||
Ты получил:
|
||||
|
||||
> систему управления инженерной эксплуатационной компанией через измеримые риски и SLA
|
||||
|
||||
|
||||
|
||||
Это уровень компаний, которые продают не услуги — а надежность инфраструктуры бизнеса.
|
||||
@@ -0,0 +1,520 @@
|
||||
Ты сейчас описываешь переход от “владельца инженерной фирмы” к роли:
|
||||
|
||||
операционного директора инженерной системы (Engineering Operations System Owner)
|
||||
|
||||
В такой модели ты уже не контролируешь людей напрямую — ты контролируешь поток рисков, денег, SLA и загрузки системы.
|
||||
|
||||
Я разложу это как управленческую “панель управления” (CEO Dashboard), разделив на:
|
||||
|
||||
1. критически важные метрики (must have)
|
||||
|
||||
|
||||
2. важные (should have)
|
||||
|
||||
|
||||
3. полезные (nice to have)
|
||||
|
||||
|
||||
4. формулы
|
||||
|
||||
|
||||
5. графики и визуализации
|
||||
|
||||
|
||||
6. как это связывается в одну систему управления
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. КРИТИЧЕСКИ ВАЖНЫЕ ПАРАМЕТРЫ (CEO CORE CONTROL LAYER)
|
||||
|
||||
Это то, без чего ты не управляешь компанией, а просто “наблюдаешь бизнес”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
1.1 MRR / ARR (регулярный доход SLA)
|
||||
|
||||
Формула:
|
||||
|
||||
MRR = Σ (все SLA контракты / 12)
|
||||
ARR = MRR × 12
|
||||
|
||||
|
||||
---
|
||||
|
||||
Почему это главное:
|
||||
|
||||
Ты продаёшь не монтаж, а:
|
||||
|
||||
> стабильность объектов
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
График:
|
||||
|
||||
📈 линия роста MRR по месяцам
|
||||
📊 разбивка по типам объектов
|
||||
|
||||
|
||||
---
|
||||
|
||||
Управленческая логика:
|
||||
|
||||
если MRR растёт → система здорова
|
||||
|
||||
если нет → маркетинг/воронка сломана
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1.2 RETENTION RATE (удержание клиентов)
|
||||
|
||||
Формула:
|
||||
|
||||
Retention = (клиенты в конце периода / клиенты в начале) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
95–100% = отлично
|
||||
|
||||
85–95% = нормально
|
||||
|
||||
<85% = проблема в SLA
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
График:
|
||||
|
||||
📉 “утечка клиентов по месяцам”
|
||||
|
||||
|
||||
---
|
||||
|
||||
Интерпретация:
|
||||
|
||||
Если падает retention — проблема не в продажах, а в:
|
||||
|
||||
инженерах
|
||||
|
||||
SLA
|
||||
|
||||
качестве реакции
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1.3 SLA COMPLIANCE (исполнение контрактов)
|
||||
|
||||
Формула:
|
||||
|
||||
SLA Compliance = (выполненные заявки в SLA / все заявки) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма:
|
||||
|
||||
95–99% = система стабильна
|
||||
|
||||
<90% = начинаются разрывы контрактов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
График:
|
||||
|
||||
📊 compliance по инженерам / регионам
|
||||
|
||||
|
||||
---
|
||||
|
||||
1.4 COST PER SLA OBJECT (стоимость обслуживания объекта)
|
||||
|
||||
Формула:
|
||||
|
||||
Cost per Object = (ФОТ + выезды + оборудование + накладные) / количество объектов
|
||||
|
||||
|
||||
---
|
||||
|
||||
Почему важно:
|
||||
|
||||
Ты должен видеть:
|
||||
|
||||
> объект приносит деньги или сжигает ресурс
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
График:
|
||||
|
||||
📊 распределение стоимости по типам объектов
|
||||
|
||||
|
||||
---
|
||||
|
||||
1.5 ENGINEER PRODUCTIVITY (производительность инженера)
|
||||
|
||||
Формула:
|
||||
|
||||
EP = SLA Revenue / количество инженеров
|
||||
|
||||
|
||||
---
|
||||
|
||||
Или глубже:
|
||||
|
||||
EP2 = (закрытые заявки × сложность) / часы работы
|
||||
|
||||
|
||||
---
|
||||
|
||||
График:
|
||||
|
||||
📊 эффективность по инженерам (ranking)
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. ВАЖНЫЕ ПАРАМЕТРЫ (OPTIMIZATION LAYER)
|
||||
|
||||
Это влияет на прибыль, но не ломает систему сразу.
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.1 LEAD → SLA CONVERSION RATE
|
||||
|
||||
Формула:
|
||||
|
||||
Conversion = SLA contracts / all qualified leads
|
||||
|
||||
|
||||
---
|
||||
|
||||
График:
|
||||
|
||||
📈 воронка: лиды → аудит → SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
Диагностика:
|
||||
|
||||
низкая конверсия → проблема продаж/аудита
|
||||
|
||||
высокая → система сильная
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.2 AVERAGE SLA CONTRACT VALUE
|
||||
|
||||
Формула:
|
||||
|
||||
ARPC = Total SLA revenue / number of contracts
|
||||
|
||||
|
||||
---
|
||||
|
||||
График:
|
||||
|
||||
📊 средний чек по объектам
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.3 INCIDENT RATE (частота аварий)
|
||||
|
||||
Формула:
|
||||
|
||||
IR = incidents / objects
|
||||
|
||||
|
||||
---
|
||||
|
||||
Почему важно:
|
||||
|
||||
Показывает:
|
||||
|
||||
> реальное качество эксплуатации
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.4 REOPEN RATE (повторные заявки)
|
||||
|
||||
Reopen = повторные обращения / все обращения
|
||||
|
||||
|
||||
---
|
||||
|
||||
Если растёт:
|
||||
|
||||
→ инженер плохо диагностирует
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. ПОЛЕЗНЫЕ ПАРАМЕТРЫ (NICE TO HAVE)
|
||||
|
||||
Это усиливает контроль, но не критично.
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.1 CUSTOMER SATISFACTION (CSAT)
|
||||
|
||||
CSAT = (положительные оценки / все оценки) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.2 RESPONSE TIME DISTRIBUTION
|
||||
|
||||
Не среднее, а распределение:
|
||||
|
||||
📊 0–2 часа
|
||||
📊 2–6
|
||||
📊 6–24
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.3 EQUIPMENT HEALTH INDEX
|
||||
|
||||
EHI = (исправное оборудование / общее оборудование) × 100%
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.4 ENGINEER UTILIZATION RATE
|
||||
|
||||
Utilization = рабочие часы / доступные часы
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ГЛАВНЫЙ CEO-ИНДЕКС (SYSTEM HEALTH SCORE)
|
||||
|
||||
Это твоя “панель управления бизнесом”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
SHS =
|
||||
(0.25 × MRR growth) +
|
||||
(0.20 × SLA compliance) +
|
||||
(0.20 × retention) +
|
||||
(0.15 × engineer productivity) +
|
||||
(0.10 × conversion rate) +
|
||||
(0.10 × incident stability)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Интерпретация:
|
||||
|
||||
SHS состояние
|
||||
|
||||
85–100 масштабируемый рост
|
||||
70–85 стабильный бизнес
|
||||
50–70 проблемы в системе
|
||||
<50 системный кризис
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. КЛЮЧЕВЫЕ ГРАФИКИ CEO (ДОСКА УПРАВЛЕНИЯ)
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.1 MRR Growth Curve
|
||||
|
||||
📈 рост повторного дохода
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.2 SLA Compliance Heatmap
|
||||
|
||||
🟥 красный = проблемные инженеры
|
||||
🟩 зелёный = сильные
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.3 Client Retention Funnel
|
||||
|
||||
📊 где теряются клиенты
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.4 Engineer Performance Ranking
|
||||
|
||||
📊 топ / низ производительности
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.5 Incident Map
|
||||
|
||||
📍 какие объекты ломаются чаще
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.6 Cost vs Revenue per Object
|
||||
|
||||
📊 прибыльность каждого SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. КРИТИЧЕСКАЯ ЛОГИКА УПРАВЛЕНИЯ
|
||||
|
||||
Ты управляешь не людьми.
|
||||
|
||||
Ты управляешь:
|
||||
|
||||
рисками → SLA → деньгами → нагрузкой инженеров
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. ЧТО ТЕБЕ ОБЯЗАТЕЛЬНО ВИДЕТЬ КАЖДЫЙ ДЕНЬ
|
||||
|
||||
|
||||
---
|
||||
|
||||
DAILY CEO DASHBOARD:
|
||||
|
||||
SLA compliance
|
||||
|
||||
аварии за 24ч
|
||||
|
||||
новые SLA контракты
|
||||
|
||||
просрочки
|
||||
|
||||
перегруз инженеров
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. ЧТО ТЕБЕ НУЖНО ВИДЕТЬ РАЗ В НЕДЕЛЮ
|
||||
|
||||
|
||||
---
|
||||
|
||||
MRR рост
|
||||
|
||||
retention
|
||||
|
||||
эффективность инженеров
|
||||
|
||||
конверсия аудита в SLA
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. ЧТО СМОТРЕТЬ РАЗ В МЕСЯЦ
|
||||
|
||||
|
||||
---
|
||||
|
||||
прибыльность объектов
|
||||
|
||||
эффективность регионов
|
||||
|
||||
нагрузка команды
|
||||
|
||||
системные сбои
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
10. ГЛАВНЫЙ ПРИНЦИП
|
||||
|
||||
Ты не управляешь:
|
||||
|
||||
❌ монтажами
|
||||
❌ выездами
|
||||
❌ людьми
|
||||
|
||||
|
||||
---
|
||||
|
||||
Ты управляешь:
|
||||
|
||||
системой инженерной надежности объектов бизнеса
|
||||
|
||||
|
||||
---
|
||||
|
||||
11. ЕСЛИ СВЯЗАТЬ ВСЁ В ОДНУ МОДЕЛЬ
|
||||
|
||||
Аудит → Risk Score → SLA цена → контракт → MRR → KPI инженеров → retention → рост
|
||||
|
||||
|
||||
---
|
||||
|
||||
12. ИТОГ
|
||||
|
||||
Ты как CEO должен видеть не “что происходит”, а:
|
||||
|
||||
> где именно в системе возникает прибыль или утечка денег/риска
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
13. ЕСЛИ ДАЛЬШЕ РАЗВИВАТЬ СИСТЕМУ
|
||||
|
||||
Я могу собрать:
|
||||
|
||||
полноценный CEO-dashboard (Excel / Notion / Power BI)
|
||||
|
||||
автоматическую систему расчёта SHS
|
||||
|
||||
дашборд инженеров в реальном времени
|
||||
|
||||
модель прогнозирования MRR на 3–6 месяцев
|
||||
|
||||
систему раннего предупреждения потерь клиентов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Если коротко:
|
||||
|
||||
ты строишь не компанию — ты строишь управляемую инженерную систему с финансовыми законами внутри неё.
|
||||
@@ -0,0 +1,488 @@
|
||||
Сейчас мы подходим к одному из самых важных этапов.
|
||||
Потому что SEO для тебя — это НЕ “трафик ради трафика”.
|
||||
|
||||
Тебе не нужны:
|
||||
|
||||
- тысячи посетителей;
|
||||
- B2C заявки;
|
||||
- мусорные лиды.
|
||||
|
||||
Тебе нужен:
|
||||
|
||||
# узкий B2B SEO с высокой экспертностью.
|
||||
|
||||
И здесь у тебя огромный шанс, потому что рынок безопасности:
|
||||
|
||||
# SEO делает ужасно.
|
||||
|
||||
Большинство сайтов:
|
||||
|
||||
- про “монтаж камер”;
|
||||
- про оборудование;
|
||||
- про бренды;
|
||||
- про “низкие цены”.
|
||||
|
||||
А ты можешь занять:
|
||||
|
||||
# нишу инженерной эксплуатации.
|
||||
|
||||
---
|
||||
|
||||
# ГЛАВНАЯ СТРАТЕГИЯ SEO
|
||||
|
||||
---
|
||||
|
||||
# НЕ ПРОДАВАТЬ:
|
||||
|
||||
- камеры;
|
||||
- оборудование;
|
||||
- монтаж.
|
||||
|
||||
---
|
||||
|
||||
# ПРОДАВАТЬ:
|
||||
|
||||
- эксплуатацию;
|
||||
- SLA;
|
||||
- аудит;
|
||||
- контроль;
|
||||
- сопровождение;
|
||||
- надежность.
|
||||
|
||||
---
|
||||
|
||||
# ТВОЯ SEO-МОДЕЛЬ
|
||||
|
||||
---
|
||||
|
||||
# УРОВЕНЬ 1
|
||||
|
||||
# КОММЕРЧЕСКИЕ СТРАНИЦЫ
|
||||
|
||||
Это:
|
||||
|
||||
# страницы услуг.
|
||||
|
||||
Они приводят клиентов.
|
||||
|
||||
---
|
||||
|
||||
# УРОВЕНЬ 2
|
||||
|
||||
# ОТРАСЛЕВЫЕ СТРАНИЦЫ
|
||||
|
||||
Это:
|
||||
|
||||
# страницы под конкретный бизнес.
|
||||
|
||||
Они повышают доверие и SEO.
|
||||
|
||||
---
|
||||
|
||||
# УРОВЕНЬ 3
|
||||
|
||||
# ЭКСПЕРТНЫЙ БЛОГ
|
||||
|
||||
Это:
|
||||
|
||||
# двигатель доверия.
|
||||
|
||||
---
|
||||
|
||||
# СТРУКТУРА САЙТА
|
||||
|
||||
Вот идеальная структура для тебя.
|
||||
|
||||
---
|
||||
|
||||
# ГЛАВНАЯ
|
||||
|
||||
## URL:
|
||||
|
||||
`/`
|
||||
|
||||
---
|
||||
|
||||
## Цель:
|
||||
|
||||
- показать позиционирование;
|
||||
- перевести из “монтажников”;
|
||||
- дать B2B доверие.
|
||||
|
||||
---
|
||||
|
||||
# РАЗДЕЛ УСЛУГ
|
||||
|
||||
---
|
||||
|
||||
## `/services/`
|
||||
|
||||
Общая страница услуг.
|
||||
|
||||
---
|
||||
|
||||
# ОСНОВНЫЕ СТРАНИЦЫ
|
||||
|
||||
---
|
||||
|
||||
## `/services/audit/`
|
||||
|
||||
# Технический аудит систем безопасности
|
||||
|
||||
---
|
||||
|
||||
## `/services/sla/`
|
||||
|
||||
# SLA сопровождение объектов
|
||||
|
||||
---
|
||||
|
||||
## `/services/maintenance/`
|
||||
|
||||
# Техническое обслуживание систем безопасности
|
||||
|
||||
---
|
||||
|
||||
## `/services/external-engineer/`
|
||||
|
||||
# Внешний инженер безопасности
|
||||
|
||||
---
|
||||
|
||||
## `/services/recovery/`
|
||||
|
||||
# Восстановление проблемных объектов
|
||||
|
||||
---
|
||||
|
||||
## `/services/fire-alarm/`
|
||||
|
||||
# Обслуживание пожарной сигнализации
|
||||
|
||||
---
|
||||
|
||||
## `/services/video-surveillance/`
|
||||
|
||||
# Эксплуатация видеонаблюдения
|
||||
|
||||
---
|
||||
|
||||
## `/services/access-control/`
|
||||
|
||||
# Обслуживание СКУД
|
||||
|
||||
---
|
||||
|
||||
# ОТРАСЛЕВЫЕ СТРАНИЦЫ
|
||||
|
||||
Это очень важно.
|
||||
|
||||
---
|
||||
|
||||
## `/industries/hotels/`
|
||||
|
||||
# Системы безопасности гостиниц
|
||||
|
||||
---
|
||||
|
||||
## `/industries/logistics/`
|
||||
|
||||
# Склады и логистика
|
||||
|
||||
---
|
||||
|
||||
## `/industries/medical/`
|
||||
|
||||
# Клиники и медцентры
|
||||
|
||||
---
|
||||
|
||||
## `/industries/commercial/`
|
||||
|
||||
# Коммерческая недвижимость
|
||||
|
||||
---
|
||||
|
||||
## `/industries/agro/`
|
||||
|
||||
# Агропредприятия
|
||||
|
||||
---
|
||||
|
||||
# БЛОГ
|
||||
|
||||
---
|
||||
|
||||
## `/blog/`
|
||||
|
||||
---
|
||||
|
||||
# КАТЕГОРИИ
|
||||
|
||||
---
|
||||
|
||||
## `/blog/video/`
|
||||
|
||||
Проблемы видеонаблюдения.
|
||||
|
||||
---
|
||||
|
||||
## `/blog/fire-alarm/`
|
||||
|
||||
Пожарная безопасность.
|
||||
|
||||
---
|
||||
|
||||
## `/blog/sla/`
|
||||
|
||||
Эксплуатация и SLA.
|
||||
|
||||
---
|
||||
|
||||
## `/blog/audit/`
|
||||
|
||||
Технический аудит.
|
||||
|
||||
---
|
||||
|
||||
# ПОЧЕМУ ЭТО ТАК ВАЖНО
|
||||
|
||||
Google и Яндекс сейчас любят:
|
||||
|
||||
- структуру;
|
||||
- экспертность;
|
||||
- тематичность;
|
||||
- глубину.
|
||||
|
||||
---
|
||||
|
||||
# ЧТО НЕЛЬЗЯ ДЕЛАТЬ
|
||||
|
||||
---
|
||||
|
||||
# НЕ ДЕЛАТЬ:
|
||||
|
||||
- 1 страницу “все услуги”;
|
||||
- короткие SEO-тексты;
|
||||
- переспам;
|
||||
- “установка камер Краснодар”.
|
||||
|
||||
Это мусорный рынок.
|
||||
|
||||
---
|
||||
|
||||
# ТВОЯ SEO-СТРАТЕГИЯ
|
||||
|
||||
---
|
||||
|
||||
# НЕ:
|
||||
|
||||
“дешевый монтаж”
|
||||
|
||||
---
|
||||
|
||||
# А:
|
||||
|
||||
# “эксплуатационные риски объектов”.
|
||||
|
||||
---
|
||||
|
||||
# ГЛАВНЫЕ SEO КЛЮЧИ
|
||||
|
||||
Вот где деньги.
|
||||
|
||||
---
|
||||
|
||||
# АУДИТ
|
||||
|
||||
- аудит систем безопасности
|
||||
- технический аудит объекта
|
||||
- аудит видеонаблюдения
|
||||
- аудит пожарной сигнализации
|
||||
- проверка систем безопасности
|
||||
|
||||
---
|
||||
|
||||
# SLA
|
||||
|
||||
- SLA обслуживание
|
||||
- сопровождение систем безопасности
|
||||
- техническое сопровождение объекта
|
||||
- эксплуатация систем безопасности
|
||||
|
||||
---
|
||||
|
||||
# ОБСЛУЖИВАНИЕ
|
||||
|
||||
- обслуживание пожарной сигнализации Краснодар
|
||||
- обслуживание видеонаблюдения Краснодар
|
||||
- обслуживание СКУД Краснодар
|
||||
|
||||
---
|
||||
|
||||
# ОТРАСЛЕВЫЕ
|
||||
|
||||
- системы безопасности гостиниц
|
||||
- обслуживание гостиниц Краснодар
|
||||
- безопасность складов Краснодар
|
||||
|
||||
---
|
||||
|
||||
# ТЕПЕРЬ ГЛАВНОЕ
|
||||
|
||||
# КАК ПИСАТЬ SEO-ТЕКСТЫ
|
||||
|
||||
---
|
||||
|
||||
# ТВОЯ ЦЕЛЬ
|
||||
|
||||
НЕ: “набить ключи”.
|
||||
|
||||
---
|
||||
|
||||
# А:
|
||||
|
||||
# показать инженерную экспертность.
|
||||
|
||||
---
|
||||
|
||||
# ПРИМЕР SEO-ТЕКСТА
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# ТЕПЕРЬ:
|
||||
|
||||
# КОД СТРАНИЦЫ УСЛУГИ
|
||||
|
||||
Например:
|
||||
|
||||
# `/services/audit/index.php`
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# SEO РЕКОМЕНДАЦИИ ПО ТЕХНИЧЕСКОЙ ЧАСТИ
|
||||
|
||||
---
|
||||
|
||||
# ОБЯЗАТЕЛЬНО:
|
||||
|
||||
---
|
||||
|
||||
# 1. ЧПУ URL
|
||||
|
||||
---
|
||||
|
||||
# ХОРОШО:
|
||||
|
||||
`/services/audit/`
|
||||
|
||||
---
|
||||
|
||||
# ПЛОХО:
|
||||
|
||||
`/page.php?id=123`
|
||||
|
||||
---
|
||||
|
||||
# 2. TITLE И DESCRIPTION
|
||||
|
||||
Для каждой страницы.
|
||||
|
||||
---
|
||||
|
||||
# Пример:
|
||||
|
||||
## TITLE:
|
||||
|
||||
Технический аудит систем безопасности в Краснодаре
|
||||
|
||||
---
|
||||
|
||||
## DESCRIPTION:
|
||||
|
||||
Проверка состояния систем безопасности коммерческих объектов. Аудит видеонаблюдения, СКУД и пожарной сигнализации.
|
||||
|
||||
---
|
||||
|
||||
# 3. H1 ТОЛЬКО ОДИН
|
||||
|
||||
Это важно.
|
||||
|
||||
---
|
||||
|
||||
# 4. schema.org
|
||||
|
||||
Очень желательно.
|
||||
|
||||
---
|
||||
|
||||
# 5. СКОРОСТЬ
|
||||
|
||||
Не перегружать:
|
||||
|
||||
- анимацией;
|
||||
- тяжелыми видео;
|
||||
- конструкторами.
|
||||
|
||||
---
|
||||
|
||||
# 6. GEO SEO
|
||||
|
||||
На каждой странице:
|
||||
|
||||
- Краснодар;
|
||||
- Краснодарский край.
|
||||
|
||||
---
|
||||
|
||||
# 7. ВНУТРЕННЯЯ ПЕРЕЛИНКОВКА
|
||||
|
||||
Например:
|
||||
|
||||
- из статьи → на аудит;
|
||||
- из отрасли → на SLA;
|
||||
- из кейса → на услугу.
|
||||
|
||||
---
|
||||
|
||||
# ЧТО ДАСТ РЕАЛЬНЫЙ РОСТ
|
||||
|
||||
---
|
||||
|
||||
# НЕ:
|
||||
|
||||
- покупка ссылок;
|
||||
- SEO-магия.
|
||||
|
||||
---
|
||||
|
||||
# А:
|
||||
|
||||
- экспертные статьи;
|
||||
- кейсы;
|
||||
- отраслевые страницы;
|
||||
- техническая глубина;
|
||||
- регулярность.
|
||||
|
||||
---
|
||||
|
||||
# САМОЕ ВАЖНОЕ
|
||||
|
||||
Ты должен выглядеть:
|
||||
|
||||
# не как “сайт монтажников”.
|
||||
|
||||
А как:
|
||||
|
||||
# инженерная эксплуатационная компания.
|
||||
|
||||
И это:
|
||||
|
||||
- редкость;
|
||||
- слабоконкурентная ниша;
|
||||
- высокий уровень доверия;
|
||||
- хороший B2B SEO.
|
||||
@@ -0,0 +1,795 @@
|
||||
SLA — ЭТО ОСНОВА ТВОЕГО ПЕРЕХОДА ИЗ “МОНТАЖНИКОВ” В B2B-СЕРВИС
|
||||
|
||||
Большинство компаний безопасности в Краснодарском крае:
|
||||
|
||||
“иногда приезжают”;
|
||||
|
||||
“что-то смотрят”;
|
||||
|
||||
“делают отметку”;
|
||||
|
||||
работают хаотично.
|
||||
|
||||
|
||||
SLA делает тебя:
|
||||
|
||||
инженерной сервисной компанией;
|
||||
|
||||
предсказуемым подрядчиком;
|
||||
|
||||
частью эксплуатации бизнеса клиента.
|
||||
|
||||
|
||||
Именно SLA позволяет:
|
||||
|
||||
продавать дороже;
|
||||
|
||||
получать абонентку;
|
||||
|
||||
заходить в сети;
|
||||
|
||||
работать с коммерческой недвижимостью;
|
||||
|
||||
уходить от демпинга.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЧТО ТАКОЕ SLA ПРОСТЫМИ СЛОВАМИ
|
||||
|
||||
SLA (Service Level Agreement)
|
||||
|
||||
это:
|
||||
|
||||
соглашение об уровне сервиса.
|
||||
|
||||
То есть: ты заранее фиксируешь:
|
||||
|
||||
что обслуживаешь;
|
||||
|
||||
как быстро реагируешь;
|
||||
|
||||
что считается аварией;
|
||||
|
||||
что входит в обслуживание;
|
||||
|
||||
какие обязательства у сторон.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПОЧЕМУ B2B ЭТО ЛЮБИТ
|
||||
|
||||
Потому что бизнес покупает:
|
||||
|
||||
предсказуемость;
|
||||
|
||||
контроль;
|
||||
|
||||
ответственность;
|
||||
|
||||
понятные сроки.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТВОЯ ЦЕЛЬ
|
||||
|
||||
НЕ:
|
||||
|
||||
> “мы обслуживаем камеры”
|
||||
|
||||
|
||||
|
||||
А:
|
||||
|
||||
“мы гарантируем работоспособность критической инфраструктуры объекта”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
КАК SLA ПОДНИМАЕТ ТЕБЯ НАД РЫНКОМ
|
||||
|
||||
Без SLA:
|
||||
|
||||
“звоните если что”.
|
||||
|
||||
С SLA:
|
||||
|
||||
регламенты;
|
||||
|
||||
сроки реакции;
|
||||
|
||||
журналирование;
|
||||
|
||||
прозрачность;
|
||||
|
||||
ответственность.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ГЛАВНАЯ ОШИБКА
|
||||
|
||||
Многие делают:
|
||||
|
||||
> “приедем в течение суток”.
|
||||
|
||||
|
||||
|
||||
Это НЕ SLA.
|
||||
|
||||
|
||||
---
|
||||
|
||||
НАСТОЯЩИЙ SLA ВКЛЮЧАЕТ
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. КЛАССИФИКАЦИЮ ИНЦИДЕНТОВ
|
||||
|
||||
Например:
|
||||
|
||||
Приоритет Пример Реакция
|
||||
|
||||
P1 Критический Не работает пожарка 2 часа
|
||||
P2 Высокий Нет архива камер 4 часа
|
||||
P3 Средний Частично не работает СКУД 24 часа
|
||||
P4 Низкий Настройка доступа Планово
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. ВРЕМЯ РЕАКЦИИ
|
||||
|
||||
Очень важно: не “устранения”.
|
||||
|
||||
А:
|
||||
|
||||
начала работ.
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. РЕГЛАМЕНТНЫЕ РАБОТЫ
|
||||
|
||||
Например:
|
||||
|
||||
ежемесячные проверки;
|
||||
|
||||
тестирование;
|
||||
|
||||
резервное копирование;
|
||||
|
||||
контроль архивов;
|
||||
|
||||
проверка питания;
|
||||
|
||||
чистка;
|
||||
|
||||
тест тревог.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ОТЧЕТНОСТЬ
|
||||
|
||||
Это критично.
|
||||
|
||||
Ты должен быть:
|
||||
|
||||
“прозрачным подрядчиком”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. SLA ПО КАНАЛАМ СВЯЗИ
|
||||
|
||||
Например:
|
||||
|
||||
Telegram;
|
||||
|
||||
email;
|
||||
|
||||
аварийный телефон;
|
||||
|
||||
сервисный портал.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. SLA ПО ДОСТУПНОСТИ
|
||||
|
||||
Вот тут начинаются большие чеки.
|
||||
|
||||
Например:
|
||||
|
||||
Система Целевая доступность
|
||||
|
||||
Пожарка 99.9%
|
||||
Видеонаблюдение 99%
|
||||
СКУД 99%
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕПЕРЬ ГЛАВНОЕ:
|
||||
|
||||
КАК ТЕБЕ ДЕЛАТЬ SLA В РЕАЛЬНОСТИ
|
||||
|
||||
Тебе НЕ нужен корпоративный монстр.
|
||||
|
||||
Тебе нужна:
|
||||
|
||||
простая инженерная система.
|
||||
|
||||
|
||||
---
|
||||
|
||||
БАЗОВАЯ МОДЕЛЬ SLA ДЛЯ ТЕБЯ
|
||||
|
||||
|
||||
---
|
||||
|
||||
START SLA
|
||||
|
||||
Для:
|
||||
|
||||
небольших объектов;
|
||||
|
||||
офисов;
|
||||
|
||||
клиник;
|
||||
|
||||
магазинов.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Реакция:
|
||||
|
||||
до 24 часов
|
||||
|
||||
|
||||
---
|
||||
|
||||
Регламент:
|
||||
|
||||
1 раз в месяц.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Что входит:
|
||||
|
||||
проверка состояния;
|
||||
|
||||
диагностика;
|
||||
|
||||
журнал;
|
||||
|
||||
рекомендации;
|
||||
|
||||
консультации.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Стоимость:
|
||||
|
||||
25–40 тыс ₽/мес
|
||||
|
||||
Для Краснодара — нормально для качественного B2B.
|
||||
|
||||
|
||||
---
|
||||
|
||||
BUSINESS SLA
|
||||
|
||||
Твой основной продукт.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Реакция:
|
||||
|
||||
критическая авария — 4 часа;
|
||||
|
||||
обычная — 24 часа.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Регламент:
|
||||
|
||||
2–4 раза в месяц.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Входит:
|
||||
|
||||
аварийные выезды;
|
||||
|
||||
удаленная диагностика;
|
||||
|
||||
контроль архива;
|
||||
|
||||
контроль питания;
|
||||
|
||||
фотоотчеты;
|
||||
|
||||
журнал;
|
||||
|
||||
рекомендации;
|
||||
|
||||
сопровождение проверок.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Стоимость:
|
||||
|
||||
60–120 тыс ₽/мес
|
||||
|
||||
Для:
|
||||
|
||||
гостиниц;
|
||||
|
||||
складов;
|
||||
|
||||
коммерческой недвижимости.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ENTERPRISE SLA
|
||||
|
||||
Вот это путь к большим деньгам.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Реакция:
|
||||
|
||||
2 часа
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формат:
|
||||
|
||||
“внешний инженерный отдел”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Входит:
|
||||
|
||||
постоянный контроль;
|
||||
|
||||
участие в эксплуатации;
|
||||
|
||||
работа с подрядчиками;
|
||||
|
||||
аудит;
|
||||
|
||||
сопровождение модернизаций;
|
||||
|
||||
приемка;
|
||||
|
||||
развитие систем.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Стоимость:
|
||||
|
||||
180–500 тыс ₽/мес
|
||||
|
||||
|
||||
---
|
||||
|
||||
ВАЖНО:
|
||||
|
||||
SLA НЕЛЬЗЯ ПРОДАВАТЬ КАК “ТО”
|
||||
|
||||
Иначе: ты вернешься в дешевый рынок.
|
||||
|
||||
|
||||
---
|
||||
|
||||
SLA ПРОДАЕТСЯ КАК:
|
||||
|
||||
“управление рисками объекта”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПРИМЕР ТЕКСТА ДЛЯ САЙТА
|
||||
|
||||
|
||||
---
|
||||
|
||||
SLA-сопровождение систем безопасности
|
||||
|
||||
Мы работаем по регламентированным SLA-моделям обслуживания.
|
||||
|
||||
Это означает:
|
||||
— фиксированное время реакции;
|
||||
— контроль состояния систем;
|
||||
— прозрачную отчетность;
|
||||
— регламентные проверки;
|
||||
— ответственность за работоспособность инфраструктуры объекта.
|
||||
|
||||
SLA позволяет снизить риски простоев, исключить скрытые неисправности и обеспечить стабильную эксплуатацию систем безопасности.
|
||||
|
||||
Для каждого объекта разрабатывается индивидуальный регламент обслуживания.
|
||||
---
|
||||
|
||||
ПРИМЕР SLA ТАБЛИЦЫ ДЛЯ САЙТА
|
||||
|
||||
|
||||
---
|
||||
|
||||
Приоритет| Описание| Реакция
|
||||
P1| Полный отказ критической системы| до 2 часов
|
||||
P2| Частичная потеря функционала| до 4 часов
|
||||
P3| Некритичная неисправность| до 24 часов
|
||||
P4| Плановые работы и настройки| по графику
|
||||
---
|
||||
|
||||
ТЕПЕРЬ САМОЕ ВАЖНОЕ
|
||||
|
||||
КАК ТЕБЕ СЧИТАТЬ ЦЕНУ SLA
|
||||
|
||||
Не “от количества камер”.
|
||||
|
||||
Это ошибка рынка.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТВОЯ МОДЕЛЬ ЦЕНООБРАЗОВАНИЯ
|
||||
|
||||
Цена считается по:
|
||||
|
||||
критичности объекта;
|
||||
|
||||
количеству систем;
|
||||
|
||||
SLA;
|
||||
|
||||
расстоянию;
|
||||
|
||||
времени реакции;
|
||||
|
||||
рискам;
|
||||
|
||||
сложности эксплуатации.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
МОДЕЛЬ ДЛЯ КРАСНОДАРСКОГО КРАЯ
|
||||
|
||||
|
||||
---
|
||||
|
||||
КРАСНОДАР
|
||||
|
||||
Можно:
|
||||
|
||||
быстрые выезды;
|
||||
|
||||
дешевле логистика.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
СОЧИ
|
||||
|
||||
Цена должна быть:
|
||||
|
||||
выше на 30–50%.
|
||||
|
||||
Из-за:
|
||||
|
||||
логистики;
|
||||
|
||||
сезонности;
|
||||
|
||||
срочности.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
УДАЛЕННЫЕ РАЙОНЫ
|
||||
|
||||
Нужно:
|
||||
|
||||
отдельное SLA.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Например:
|
||||
|
||||
Базовая реакция:
|
||||
|
||||
24 часа.
|
||||
|
||||
Срочный выезд:
|
||||
|
||||
дополнительная ставка.
|
||||
|
||||
|
||||
---
|
||||
|
||||
КАК СЧИТАТЬ SLA
|
||||
|
||||
Вот модель.
|
||||
|
||||
|
||||
---
|
||||
|
||||
БАЗА
|
||||
|
||||
Например:
|
||||
|
||||
35 000 ₽
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПЛЮС:
|
||||
|
||||
Параметр Доплата
|
||||
|
||||
24/7 доступность +20%
|
||||
SLA 2 часа +35%
|
||||
Удаленный район +15–40%
|
||||
Несколько объектов индивидуально
|
||||
Высокая критичность +25%
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПРИМЕР
|
||||
|
||||
Гостиница:
|
||||
|
||||
64 камеры;
|
||||
|
||||
СКУД;
|
||||
|
||||
пожарка;
|
||||
|
||||
архив;
|
||||
|
||||
Краснодар.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
SLA:
|
||||
|
||||
4 часа.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Цена:
|
||||
|
||||
85–120 тыс ₽/мес
|
||||
|
||||
И это нормальный рынок для качественного сервиса.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕПЕРЬ САМОЕ ВАЖНОЕ
|
||||
|
||||
КАК ТЕБЕ ВЫПОЛНЯТЬ SLA БЕЗ БОЛИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
НЕ БРАТЬ ВСЕХ ПОДРЯД
|
||||
|
||||
Только:
|
||||
|
||||
нормальные объекты;
|
||||
|
||||
нормальные бюджеты;
|
||||
|
||||
адекватные заказчики.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
НЕ ПРОДАВАТЬ “ДЕШЕВО”
|
||||
|
||||
Иначе SLA развалится.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ОБЯЗАТЕЛЬНО:
|
||||
|
||||
журнал заявок;
|
||||
|
||||
регламент;
|
||||
|
||||
чек-листы;
|
||||
|
||||
фотофиксация;
|
||||
|
||||
история обслуживания.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕХНИЧЕСКИ ТЕБЕ НУЖНО
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. HELP DESK
|
||||
|
||||
Минимум:
|
||||
|
||||
Telegram;
|
||||
|
||||
1С;
|
||||
|
||||
Google Sheets.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Лучше:
|
||||
|
||||
GLPI;
|
||||
|
||||
Zammad;
|
||||
|
||||
Jira Service Desk.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. БАЗА ОБЪЕКТОВ
|
||||
|
||||
Для каждого:
|
||||
|
||||
схемы;
|
||||
|
||||
пароли;
|
||||
|
||||
оборудование;
|
||||
|
||||
IP;
|
||||
|
||||
контакты;
|
||||
|
||||
история.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. ЧЕК-ЛИСТЫ
|
||||
|
||||
Например:
|
||||
|
||||
Видеонаблюдение:
|
||||
|
||||
запись;
|
||||
|
||||
архив;
|
||||
|
||||
питание;
|
||||
|
||||
доступ;
|
||||
|
||||
фокус;
|
||||
|
||||
время.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. SLA-ДОКУМЕНТ
|
||||
|
||||
Это обязательно.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПРИМЕР СТРУКТУРЫ SLA ДОГОВОРА
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. Предмет SLA
|
||||
|
||||
Исполнитель обеспечивает техническое сопровождение
|
||||
и контроль работоспособности систем безопасности объекта.
|
||||
|
||||
2. Время реакции
|
||||
|
||||
P1 — до 2 часов
|
||||
P2 — до 4 часов
|
||||
P3 — до 24 часов
|
||||
|
||||
3. Регламентные работы
|
||||
|
||||
— диагностика
|
||||
— тестирование
|
||||
— проверка журналов
|
||||
— контроль архива
|
||||
— проверка питания
|
||||
|
||||
4. Отчетность
|
||||
|
||||
По результатам работ предоставляется отчет.
|
||||
|
||||
5. Ограничения SLA
|
||||
|
||||
SLA не распространяется на:
|
||||
— физическое повреждение оборудования
|
||||
— аварии электропитания
|
||||
— действия третьих лиц
|
||||
— форс-мажор
|
||||
---
|
||||
|
||||
ИТОГ
|
||||
|
||||
SLA — это:
|
||||
|
||||
не “дополнение”.
|
||||
|
||||
Это:
|
||||
|
||||
фундамент твоего нового бизнеса.
|
||||
|
||||
Именно SLA:
|
||||
|
||||
выводит тебя из монтажников;
|
||||
|
||||
делает recurring revenue;
|
||||
|
||||
дает стабильность;
|
||||
|
||||
повышает чек;
|
||||
|
||||
позволяет масштабироваться.
|
||||
|
||||
|
||||
И это идеально совпадает с твоим профилем инженера и системного человека.
|
||||
@@ -0,0 +1,69 @@
|
||||
# SQL-запросы для администрирования
|
||||
|
||||
## Как выполнить
|
||||
|
||||
1. Войдите в phpMyAdmin на хостинге
|
||||
2. Выберите базу данных `j30663893_engineering`
|
||||
3. Перейдите на вкладку «SQL»
|
||||
4. Вставьте запрос и нажмите «Вперёд»
|
||||
|
||||
## Смена пароля владельца
|
||||
|
||||
```sql
|
||||
UPDATE users
|
||||
SET password_hash = '$2y$10$e0MYzXyjpJS7k0ggKtqjGOcMFU.h9S1sGqf3qJ6qJ6qJ6qJ6qJ6qO'
|
||||
WHERE login = 'ваш_логин';
|
||||
```
|
||||
|
||||
**Важно:** Замените хеш на реальный. Для генерации хеша используйте PHP:
|
||||
|
||||
```php
|
||||
<?php echo password_hash('новый_пароль', PASSWORD_DEFAULT); ?>
|
||||
```
|
||||
|
||||
Создайте файл `hash.php` в корне сайта с этим кодом, откройте в браузере, скопируйте хеш, удалите файл.
|
||||
|
||||
## Разблокировка пользователя
|
||||
|
||||
```sql
|
||||
DELETE FROM login_attempts WHERE login = 'логин_пользователя';
|
||||
```
|
||||
|
||||
## Активация всех кейсов на главной
|
||||
|
||||
```sql
|
||||
UPDATE cases SET is_active = 1;
|
||||
```
|
||||
|
||||
## Просмотр всех пользователей
|
||||
|
||||
```sql
|
||||
SELECT id, login, role, full_name, is_active, created_at FROM users;
|
||||
```
|
||||
|
||||
## Просмотр активных SLA контрактов
|
||||
|
||||
```sql
|
||||
SELECT sc.id, sc.contract_number, o.name as object, sc.client_name,
|
||||
sc.sla_price_monthly, sc.start_date, sc.end_date, sc.status
|
||||
FROM sla_contracts sc
|
||||
JOIN objects o ON o.id = sc.object_id
|
||||
WHERE sc.status = 'active'
|
||||
ORDER BY sc.start_date DESC;
|
||||
```
|
||||
|
||||
## Сброс опросника (удаление черновиков)
|
||||
|
||||
```sql
|
||||
DELETE qs, qa
|
||||
FROM questionnaire_sessions qs
|
||||
LEFT JOIN questionnaire_answers qa ON qa.session_id = qs.id
|
||||
WHERE qs.status != 'completed';
|
||||
```
|
||||
|
||||
## Просмотр документов и прав
|
||||
|
||||
```sql
|
||||
-- Файлы в базе не хранятся. Проверьте папку service/docs/ на хостинге.
|
||||
-- Права хранятся в service/permissions/docs_permissions.json
|
||||
```
|
||||
@@ -0,0 +1,934 @@
|
||||
Хорошо, это важный шаг — ты фактически выбираешь архитектуру бренда (brand architecture), а не просто название. От этого зависит:
|
||||
|
||||
как тебя воспринимают (монтажники vs инженерная компания),
|
||||
|
||||
как ты продаёшь SLA,
|
||||
|
||||
насколько легко масштабируешься по РФ.
|
||||
|
||||
|
||||
Я разложу каждый вариант строго по твоей модели:
|
||||
👉 аудит → эксплуатация → SLA → инженерный сервис → масштабирование
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. AegisOne Engineering
|
||||
|
||||
Суть позиционирования:
|
||||
|
||||
инженерная компания полного цикла
|
||||
|
||||
|
||||
---
|
||||
|
||||
Как воспринимается рынком:
|
||||
|
||||
“серьёзная инженерная организация”
|
||||
|
||||
ближе к проектированию / эксплуатации
|
||||
|
||||
высокий уровень доверия B2B
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Сильные стороны:
|
||||
|
||||
подходит под SLA и аудит
|
||||
|
||||
звучит “дорого и системно”
|
||||
|
||||
легко масштабируется на РФ
|
||||
|
||||
не привязан к монтажу
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
не сразу понятно “сервис”
|
||||
|
||||
нужно объяснение через сайт/контент
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Лучше всего подходит для:
|
||||
|
||||
✔ SLA обслуживание
|
||||
✔ аудит объектов
|
||||
✔ эксплуатация систем безопасности
|
||||
✔ B2B рынок (гостиницы, склады, УК)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Итог:
|
||||
|
||||
> ❗ ЛУЧШИЙ ОСНОВНОЙ БРЕНД (ядро компании)
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. AegisOne Service
|
||||
|
||||
Суть:
|
||||
|
||||
сервисная эксплуатационная компания
|
||||
|
||||
|
||||
---
|
||||
|
||||
Восприятие:
|
||||
|
||||
обслуживание
|
||||
|
||||
ремонт
|
||||
|
||||
“сервисники”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Сильные стороны:
|
||||
|
||||
понятный вход для клиентов
|
||||
|
||||
хорошо продаёт обслуживание
|
||||
|
||||
легко объяснить
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
слишком “сервис/ремонт”
|
||||
|
||||
занижает статус инженера
|
||||
|
||||
хуже для крупных B2B объектов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Лучше для:
|
||||
|
||||
✔ абонентское обслуживание
|
||||
✔ выездной сервис
|
||||
✔ мелкие и средние клиенты
|
||||
✔ поддержка SLA как продукт
|
||||
|
||||
|
||||
---
|
||||
|
||||
Итог:
|
||||
|
||||
> ⚠ можно как подразделение, но не основной бренд
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. AegisOne Security Engineering
|
||||
|
||||
Суть:
|
||||
|
||||
инженерия безопасности (очень сильный B2B бренд)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Восприятие:
|
||||
|
||||
высокий уровень экспертизы
|
||||
|
||||
ближе к проектированию и системной безопасности
|
||||
|
||||
звучит “международно”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Сильные стороны:
|
||||
|
||||
максимальная экспертность
|
||||
|
||||
идеально под аудит и SLA
|
||||
|
||||
хорошо для РФ + будущего выхода за регион
|
||||
|
||||
не “монтажники”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
длинное название
|
||||
|
||||
чуть тяжеловесно в маркетинге
|
||||
|
||||
требует сокращений (AegisOne SE)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Лучше для:
|
||||
|
||||
✔ аудит
|
||||
✔ техническая экспертиза
|
||||
✔ корпоративные объекты
|
||||
✔ крупные B2B сделки
|
||||
✔ федеральное масштабирование
|
||||
|
||||
|
||||
---
|
||||
|
||||
Итог:
|
||||
|
||||
> ⭐ ЛУЧШИЙ ДЛЯ ЭКСПЕРТНОСТИ И PREMIUM B2B
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. AegisOne Facility Security
|
||||
|
||||
Суть:
|
||||
|
||||
безопасность объектов (facility management)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Восприятие:
|
||||
|
||||
управление объектами
|
||||
|
||||
эксплуатация зданий
|
||||
|
||||
FM-компании уровня “facility management”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Сильные стороны:
|
||||
|
||||
очень сильный корпоративный сегмент
|
||||
|
||||
подходит для УК, ТЦ, гостиниц
|
||||
|
||||
легко встраивается в FM-рынок
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
менее технический фокус
|
||||
|
||||
может размывать “инженерность”
|
||||
|
||||
ближе к управлению недвижимостью
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Лучше для:
|
||||
|
||||
✔ управляющие компании
|
||||
✔ гостиничные сети
|
||||
✔ торговые центры
|
||||
✔ facility management подрядчики
|
||||
|
||||
|
||||
---
|
||||
|
||||
Итог:
|
||||
|
||||
> ⚠ хорошо как направление, но не ядро
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. AegisOne TechService
|
||||
|
||||
Суть:
|
||||
|
||||
техсервис / техническое обслуживание
|
||||
|
||||
|
||||
---
|
||||
|
||||
Восприятие:
|
||||
|
||||
ремонтная служба
|
||||
|
||||
IT/техподдержка стиль
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Сильные стороны:
|
||||
|
||||
простое и понятное
|
||||
|
||||
легко продаётся “в лоб”
|
||||
|
||||
подходит для мелких объектов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
дешёвое восприятие
|
||||
|
||||
не тянет премиум SLA
|
||||
|
||||
теряет инженерный статус
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Лучше для:
|
||||
|
||||
✔ массовое обслуживание
|
||||
✔ низкий чек рынок
|
||||
✔ быстрые заявки
|
||||
✔ простые сервисные работы
|
||||
|
||||
|
||||
---
|
||||
|
||||
Итог:
|
||||
|
||||
> ❌ не подходит под твою стратегию роста
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. РЕКОМЕНТИРУЕМАЯ СТРУКТУРА БРЕНДА (ОПТИМАЛЬНАЯ)
|
||||
|
||||
Вот как это должно выглядеть в системе:
|
||||
|
||||
|
||||
---
|
||||
|
||||
🧠 ГЛАВНЫЙ БРЕНД (основа)
|
||||
|
||||
AegisOne Engineering
|
||||
|
||||
👉 ядро:
|
||||
|
||||
SLA
|
||||
|
||||
аудит
|
||||
|
||||
эксплуатация
|
||||
|
||||
инженерный контроль
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
🔧 СЕРВИСНОЕ ПОДРАЗДЕЛЕНИЕ
|
||||
|
||||
AegisOne Service
|
||||
|
||||
👉 внутри:
|
||||
|
||||
выезды
|
||||
|
||||
ремонт
|
||||
|
||||
поддержка
|
||||
|
||||
реакция по SLA
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
🧪 ЭКСПЕРТНЫЙ КОНТУР
|
||||
|
||||
AegisOne Security Engineering
|
||||
|
||||
👉 используется для:
|
||||
|
||||
сайта экспертного уровня
|
||||
|
||||
презентаций
|
||||
|
||||
коммерческих предложений
|
||||
|
||||
крупных B2B переговоров
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
🏢 ФУТУРОЛОГИЧЕСКИЙ КОНТУР (масштаб)
|
||||
|
||||
AegisOne Facility Security
|
||||
|
||||
👉 когда выйдешь в:
|
||||
|
||||
УК
|
||||
|
||||
сети отелей
|
||||
|
||||
FM-контракты
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
⚙️ ТЕХПОДДЕРЖКА (НЕ ОСНОВНАЯ)
|
||||
|
||||
AegisOne TechService
|
||||
|
||||
👉 можно использовать как:
|
||||
|
||||
внутренний бренд
|
||||
|
||||
дешёвый сегмент (если вдруг нужен)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. ЛУЧШАЯ СТРАТЕГИЯ (РЕАЛЬНО ПРАВИЛЬНАЯ)
|
||||
|
||||
Вот как это делает сильная инженерная компания:
|
||||
|
||||
|
||||
---
|
||||
|
||||
AegisOne Engineering
|
||||
↓
|
||||
AegisOne Security Engineering (экспертиза / доверие)
|
||||
↓
|
||||
AegisOne Service (исполнение SLA)
|
||||
↓
|
||||
AegisOne Facility Security (масштаб на УК и сети)
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. ВАЖНОЕ РЕШЕНИЕ ДЛЯ ТЕБЯ
|
||||
|
||||
Если брать твою стратегию:
|
||||
|
||||
уход от монтажа
|
||||
|
||||
переход в SLA
|
||||
|
||||
рост до 2–3 млн+
|
||||
|
||||
B2B рынок
|
||||
|
||||
экспертность
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТОП-1 выбор:
|
||||
|
||||
✔ AegisOne Engineering
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТОП-2 усиление:
|
||||
|
||||
✔ AegisOne Security Engineering
|
||||
|
||||
|
||||
---
|
||||
|
||||
НЕ РЕКОМЕНДУЮ КАК ОСНОВУ:
|
||||
|
||||
Service (снижает статус)
|
||||
|
||||
TechService (дешевит бренд)
|
||||
|
||||
Facility Security (узкая ниша на будущее)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. ЕСЛИ ХОЧЕШЬ ДАЛЬШЕ УСИЛИТЬ БРЕНД
|
||||
|
||||
Я могу собрать тебе следующий уровень:
|
||||
|
||||
нейминг архитектуры сайта (как разложить домены/страницы)
|
||||
|
||||
логотип-систему (как должен выглядеть бренд инженерной компании)
|
||||
|
||||
упаковку КП под каждый бренд
|
||||
|
||||
позиционирование для тендеров
|
||||
|
||||
“как выглядеть как федеральный подрядчик при локальном бизнесе”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Если коротко:
|
||||
|
||||
> тебе нужен не набор названий, а система уровней доверия
|
||||
|
||||
|
||||
|
||||
И из всех вариантов:
|
||||
|
||||
AegisOne Engineering — фундамент бизнеса.
|
||||
|
||||
---
|
||||
|
||||
Отлично, теперь мы фиксируем **архитектуру компании как инженерной системы**, а не сайта.
|
||||
|
||||
Ты строишь не “фирму услуг”, а:
|
||||
|
||||
# AegisOne Engineering — операционную систему эксплуатации объектов
|
||||
|
||||
Ниже даю тебе:
|
||||
|
||||
1. структуру компании (как она “разложена в голове клиента”)
|
||||
2. все разделы сайта / системы
|
||||
3. документы (шаблоны, которые продают SLA сами)
|
||||
4. сценарии взаимодействия с клиентом
|
||||
5. стандарты (как ты выглядишь как федеральный подрядчик)
|
||||
|
||||
---
|
||||
|
||||
# 1. СТРУКТУРА AEGISONE ENGINEERING
|
||||
|
||||
## 🧠 ЯДРО КОМПАНИИ
|
||||
|
||||
# AegisOne Engineering
|
||||
|
||||
> Инженерная компания по эксплуатации и контролю систем безопасности объектов
|
||||
|
||||
---
|
||||
|
||||
## ФУНКЦИОНАЛЬНЫЕ БЛОКИ
|
||||
|
||||
```text
|
||||
1. Engineering Audit (аудит и диагностика)
|
||||
2. SLA Operations (эксплуатация)
|
||||
3. Incident Response (аварийные работы)
|
||||
4. Technical Supervision (контроль подрядчиков)
|
||||
5. Documentation & Compliance (документация)
|
||||
6. Risk Engineering (анализ рисков)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 2. СТРУКТУРА САЙТА (КАК ДОЛЖЕН ВЫГЛЯДЕТЬ AEGISONE)
|
||||
|
||||
---
|
||||
|
||||
# 🏠 ГЛАВНАЯ
|
||||
|
||||
## `/`
|
||||
|
||||
### Название:
|
||||
|
||||
AegisOne Engineering
|
||||
|
||||
### Смысл:
|
||||
|
||||
> инженерная эксплуатация систем безопасности объектов
|
||||
|
||||
---
|
||||
|
||||
# 3. РАЗДЕЛЫ САЙТА
|
||||
|
||||
---
|
||||
|
||||
# 3.1 ENGINEERING AUDIT
|
||||
|
||||
## `/engineering-audit/`
|
||||
|
||||
### Описание:
|
||||
|
||||
```text
|
||||
Технический аудит систем безопасности с оценкой рисков эксплуатации.
|
||||
|
||||
Проверка:
|
||||
- видеонаблюдения
|
||||
- СКУД
|
||||
- пожарной сигнализации
|
||||
- инфраструктуры объекта
|
||||
|
||||
Результат:
|
||||
инженерное заключение и карта рисков объекта
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Подразделы:
|
||||
|
||||
- `/engineering-audit/video-surveillance`
|
||||
- `/engineering-audit/access-control`
|
||||
- `/engineering-audit/fire-alarm`
|
||||
- `/engineering-audit/risk-report`
|
||||
|
||||
---
|
||||
|
||||
# 3.2 SLA OPERATIONS
|
||||
|
||||
## `/sla-operations/`
|
||||
|
||||
### Описание:
|
||||
|
||||
```text
|
||||
Постоянное техническое сопровождение объектов с регламентами и SLA.
|
||||
|
||||
Мы обеспечиваем:
|
||||
- стабильную работу систем
|
||||
- контроль состояния оборудования
|
||||
- аварийное реагирование
|
||||
- регулярные проверки
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Подразделы:
|
||||
|
||||
- `/sla-operations/start`
|
||||
- `/sla-operations/business`
|
||||
- `/sla-operations/enterprise`
|
||||
- `/sla-operations/sla-regulations`
|
||||
|
||||
---
|
||||
|
||||
# 3.3 INCIDENT RESPONSE
|
||||
|
||||
## `/incident-response/`
|
||||
|
||||
### Описание:
|
||||
|
||||
```text
|
||||
Экстренное устранение неисправностей систем безопасности.
|
||||
|
||||
Сценарии:
|
||||
- отказ видеонаблюдения
|
||||
- потеря архива
|
||||
- сбой СКУД
|
||||
- проблемы пожарной сигнализации
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Подразделы:
|
||||
|
||||
- `/incident-response/critical`
|
||||
- `/incident-response/emergency-call`
|
||||
- `/incident-response/post-incident-report`
|
||||
|
||||
---
|
||||
|
||||
# 3.4 TECHNICAL SUPERVISION
|
||||
|
||||
## `/technical-supervision/`
|
||||
|
||||
### Описание:
|
||||
|
||||
```text
|
||||
Контроль подрядчиков и проверка качества работ сторонних организаций.
|
||||
|
||||
Мы выступаем как независимый инженерный контроль объекта.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Подразделы:
|
||||
|
||||
- `/technical-supervision/contractor-check`
|
||||
- `/technical-supervision/acceptance-testing`
|
||||
- `/technical-supervision/project-review`
|
||||
|
||||
---
|
||||
|
||||
# 3.5 DOCUMENTATION & COMPLIANCE
|
||||
|
||||
## `/documentation-compliance/`
|
||||
|
||||
### Описание:
|
||||
|
||||
```text
|
||||
Ведение и восстановление инженерной документации объектов.
|
||||
|
||||
Системы:
|
||||
- схемы
|
||||
- журналы
|
||||
- паспорта оборудования
|
||||
- регламенты эксплуатации
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Подразделы:
|
||||
|
||||
- `/documentation-compliance/passport`
|
||||
- `/documentation-compliance/regulations`
|
||||
- `/documentation-compliance/reporting`
|
||||
|
||||
---
|
||||
|
||||
# 3.6 RISK ENGINEERING
|
||||
|
||||
## `/risk-engineering/`
|
||||
|
||||
### Описание:
|
||||
|
||||
```text
|
||||
Анализ рисков эксплуатации инженерных систем безопасности объекта.
|
||||
|
||||
Мы оцениваем не оборудование, а последствия его отказа.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Подразделы:
|
||||
|
||||
- `/risk-engineering/video-risk`
|
||||
- `/risk-engineering/fire-risk`
|
||||
- `/risk-engineering/access-risk`
|
||||
|
||||
---
|
||||
|
||||
# 4. ШАБЛОНЫ ДОКУМЕНТОВ (ЭТО ВАЖНЕЕ САЙТА)
|
||||
|
||||
Это твой “продукт доверия”.
|
||||
|
||||
---
|
||||
|
||||
# 4.1 ИНЖЕНЕРНОЕ ЗАКЛЮЧЕНИЕ
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Инженерное заключение по состоянию систем безопасности
|
||||
|
||||
Объект: __________
|
||||
Дата: __________
|
||||
|
||||
1. Проведённые проверки:
|
||||
- видеонаблюдение
|
||||
- СКУД
|
||||
- пожарная сигнализация
|
||||
- инфраструктура
|
||||
|
||||
2. Выявленные отклонения:
|
||||
- __________
|
||||
- __________
|
||||
|
||||
3. Критические риски:
|
||||
- __________
|
||||
|
||||
4. Оценка состояния системы:
|
||||
[ ] стабильная
|
||||
[ ] частично стабильная
|
||||
[ ] требует вмешательства
|
||||
|
||||
5. Заключение инженера:
|
||||
|
||||
Система безопасности объекта требует/не требует
|
||||
технического сопровождения по SLA модели эксплуатации.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4.2 SLA СОГЛАШЕНИЕ
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Service Level Agreement (SLA)
|
||||
|
||||
1. Объект обслуживания
|
||||
2. Перечень систем
|
||||
3. Время реакции:
|
||||
P1 — 2 часа
|
||||
P2 — 4 часа
|
||||
P3 — 24 часа
|
||||
|
||||
4. Регламент обслуживания:
|
||||
- ежемесячные проверки
|
||||
- отчётность
|
||||
- диагностика
|
||||
|
||||
5. Зоны ответственности:
|
||||
- заказчик
|
||||
- инженерная компания
|
||||
|
||||
6. Исключения:
|
||||
- повреждение третьими лицами
|
||||
- форс-мажор
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4.3 АКТ ТЕХНИЧЕСКОГО АУДИТА
|
||||
|
||||
```text
|
||||
Проверено:
|
||||
- видеонаблюдение
|
||||
- СКУД
|
||||
- пожарная система
|
||||
|
||||
Результаты:
|
||||
- __________
|
||||
|
||||
Рекомендации:
|
||||
- критические
|
||||
- важные
|
||||
- плановые
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4.4 ПАСПОРТ ОБЪЕКТА
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Паспорт инженерных систем объекта
|
||||
|
||||
Содержит:
|
||||
- схема систем
|
||||
- оборудование
|
||||
- IP-адреса
|
||||
- точки отказа
|
||||
- история обслуживания
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4.5 ОТЧЁТ ПО ИНЦИДЕНТУ
|
||||
|
||||
```text
|
||||
Описание инцидента:
|
||||
Время реакции:
|
||||
Причина:
|
||||
Последствия:
|
||||
Устранение:
|
||||
Рекомендации:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 5. СЦЕНАРИИ ВЗАИМОДЕЙСТВИЯ
|
||||
|
||||
---
|
||||
|
||||
## СЦЕНАРИЙ 1 — ВХОД
|
||||
|
||||
```text
|
||||
Контакт → мини-диагностика → аудит → отчет → SLA
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## СЦЕНАРИЙ 2 — АВАРИЯ
|
||||
|
||||
```text
|
||||
Инцидент → выезд → устранение → отчет → предложение SLA
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## СЦЕНАРИЙ 3 — ПАРТНЁРСКИЙ ВХОД
|
||||
|
||||
```text
|
||||
Партнёр → объект → аудит → сопровождение
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## СЦЕНАРИЙ 4 — ТЕНДЕР / КОММЕРЧЕСКИЙ ВХОД
|
||||
|
||||
```text
|
||||
Запрос → аудит → КП → SLA контракт
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6. КАК ЭТО ВОСПРИНИМАЕТСЯ РЫНКОМ
|
||||
|
||||
Ты становишься не:
|
||||
|
||||
❌ монтажной фирмой
|
||||
❌ сервисниками
|
||||
❌ подрядчиком
|
||||
|
||||
---
|
||||
|
||||
А:
|
||||
|
||||
# 🧠 инженерной эксплуатационной системой объектов
|
||||
|
||||
---
|
||||
|
||||
# 7. ГЛАВНЫЙ ЭФФЕКТ ЭТОЙ УПАКОВКИ
|
||||
|
||||
## Клиент думает:
|
||||
|
||||
> “Это не монтажники. Это те, кто контролирует безопасность объекта как систему.”
|
||||
|
||||
---
|
||||
|
||||
# 8. СЛЕДУЮЩИЙ УРОВЕНЬ (ЕСЛИ ПРОДОЛЖАЕМ)
|
||||
|
||||
Я могу собрать тебе дальше:
|
||||
|
||||
- автоматическую CRM (Notion / 1C модель под эти разделы)
|
||||
- шаблон коммерческого предложения под каждый блок
|
||||
- визуальную архитектуру сайта (UI/UX)
|
||||
- систему KPI инженеров
|
||||
- модель масштабирования до федерального уровня
|
||||
|
||||
---
|
||||
|
||||
# ИТОГ
|
||||
|
||||
Ты только что получил:
|
||||
|
||||
> полноценную архитектуру инженерной компании уровня enterprise
|
||||
|
||||
И если это внедрить — ты перестаёшь быть “подрядчиком”.
|
||||
|
||||
Ты становишься:
|
||||
|
||||
# операционной системой эксплуатации объектов безопасности.
|
||||
@@ -0,0 +1,459 @@
|
||||
Отлично, теперь мы фиксируем архитектуру компании как инженерной системы, а не сайта.
|
||||
|
||||
Ты строишь не “фирму услуг”, а:
|
||||
|
||||
AegisOne Engineering — операционную систему эксплуатации объектов
|
||||
|
||||
Ниже даю тебе:
|
||||
|
||||
1. структуру компании (как она “разложена в голове клиента”)
|
||||
|
||||
|
||||
2. все разделы сайта / системы
|
||||
|
||||
|
||||
3. документы (шаблоны, которые продают SLA сами)
|
||||
|
||||
|
||||
4. сценарии взаимодействия с клиентом
|
||||
|
||||
|
||||
5. стандарты (как ты выглядишь как федеральный подрядчик)
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. СТРУКТУРА AEGISONE ENGINEERING
|
||||
|
||||
🧠 ЯДРО КОМПАНИИ
|
||||
|
||||
AegisOne Engineering
|
||||
|
||||
> Инженерная компания по эксплуатации и контролю систем безопасности объектов
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ФУНКЦИОНАЛЬНЫЕ БЛОКИ
|
||||
|
||||
1. Engineering Audit (аудит и диагностика)
|
||||
2. SLA Operations (эксплуатация)
|
||||
3. Incident Response (аварийные работы)
|
||||
4. Technical Supervision (контроль подрядчиков)
|
||||
5. Documentation & Compliance (документация)
|
||||
6. Risk Engineering (анализ рисков)
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. СТРУКТУРА САЙТА (КАК ДОЛЖЕН ВЫГЛЯДЕТЬ AEGISONE)
|
||||
|
||||
|
||||
---
|
||||
|
||||
🏠 ГЛАВНАЯ
|
||||
|
||||
/
|
||||
|
||||
Название:
|
||||
|
||||
AegisOne Engineering
|
||||
|
||||
Смысл:
|
||||
|
||||
> инженерная эксплуатация систем безопасности объектов
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. РАЗДЕЛЫ САЙТА
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.1 ENGINEERING AUDIT
|
||||
|
||||
/engineering-audit/
|
||||
|
||||
Описание:
|
||||
|
||||
Технический аудит систем безопасности с оценкой рисков эксплуатации.
|
||||
|
||||
Проверка:
|
||||
- видеонаблюдения
|
||||
- СКУД
|
||||
- пожарной сигнализации
|
||||
- инфраструктуры объекта
|
||||
|
||||
Результат:
|
||||
инженерное заключение и карта рисков объекта
|
||||
|
||||
|
||||
---
|
||||
|
||||
Подразделы:
|
||||
|
||||
/engineering-audit/video-surveillance
|
||||
|
||||
/engineering-audit/access-control
|
||||
|
||||
/engineering-audit/fire-alarm
|
||||
|
||||
/engineering-audit/risk-report
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.2 SLA OPERATIONS
|
||||
|
||||
/sla-operations/
|
||||
|
||||
Описание:
|
||||
|
||||
Постоянное техническое сопровождение объектов с регламентами и SLA.
|
||||
|
||||
Мы обеспечиваем:
|
||||
- стабильную работу систем
|
||||
- контроль состояния оборудования
|
||||
- аварийное реагирование
|
||||
- регулярные проверки
|
||||
|
||||
|
||||
---
|
||||
|
||||
Подразделы:
|
||||
|
||||
/sla-operations/start
|
||||
|
||||
/sla-operations/business
|
||||
|
||||
/sla-operations/enterprise
|
||||
|
||||
/sla-operations/sla-regulations
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.3 INCIDENT RESPONSE
|
||||
|
||||
/incident-response/
|
||||
|
||||
Описание:
|
||||
|
||||
Экстренное устранение неисправностей систем безопасности.
|
||||
|
||||
Сценарии:
|
||||
- отказ видеонаблюдения
|
||||
- потеря архива
|
||||
- сбой СКУД
|
||||
- проблемы пожарной сигнализации
|
||||
|
||||
|
||||
---
|
||||
|
||||
Подразделы:
|
||||
|
||||
/incident-response/critical
|
||||
|
||||
/incident-response/emergency-call
|
||||
|
||||
/incident-response/post-incident-report
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.4 TECHNICAL SUPERVISION
|
||||
|
||||
/technical-supervision/
|
||||
|
||||
Описание:
|
||||
|
||||
Контроль подрядчиков и проверка качества работ сторонних организаций.
|
||||
|
||||
Мы выступаем как независимый инженерный контроль объекта.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Подразделы:
|
||||
|
||||
/technical-supervision/contractor-check
|
||||
|
||||
/technical-supervision/acceptance-testing
|
||||
|
||||
/technical-supervision/project-review
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.5 DOCUMENTATION & COMPLIANCE
|
||||
|
||||
/documentation-compliance/
|
||||
|
||||
Описание:
|
||||
|
||||
Ведение и восстановление инженерной документации объектов.
|
||||
|
||||
Системы:
|
||||
- схемы
|
||||
- журналы
|
||||
- паспорта оборудования
|
||||
- регламенты эксплуатации
|
||||
|
||||
|
||||
---
|
||||
|
||||
Подразделы:
|
||||
|
||||
/documentation-compliance/passport
|
||||
|
||||
/documentation-compliance/regulations
|
||||
|
||||
/documentation-compliance/reporting
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.6 RISK ENGINEERING
|
||||
|
||||
/risk-engineering/
|
||||
|
||||
Описание:
|
||||
|
||||
Анализ рисков эксплуатации инженерных систем безопасности объекта.
|
||||
|
||||
Мы оцениваем не оборудование, а последствия его отказа.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Подразделы:
|
||||
|
||||
/risk-engineering/video-risk
|
||||
|
||||
/risk-engineering/fire-risk
|
||||
|
||||
/risk-engineering/access-risk
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ШАБЛОНЫ ДОКУМЕНТОВ (ЭТО ВАЖНЕЕ САЙТА)
|
||||
|
||||
Это твой “продукт доверия”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.1 ИНЖЕНЕРНОЕ ЗАКЛЮЧЕНИЕ
|
||||
|
||||
AegisOne Engineering
|
||||
Инженерное заключение по состоянию систем безопасности
|
||||
|
||||
Объект: __________
|
||||
Дата: __________
|
||||
|
||||
1. Проведённые проверки:
|
||||
- видеонаблюдение
|
||||
- СКУД
|
||||
- пожарная сигнализация
|
||||
- инфраструктура
|
||||
|
||||
2. Выявленные отклонения:
|
||||
- __________
|
||||
- __________
|
||||
|
||||
3. Критические риски:
|
||||
- __________
|
||||
|
||||
4. Оценка состояния системы:
|
||||
[ ] стабильная
|
||||
[ ] частично стабильная
|
||||
[ ] требует вмешательства
|
||||
|
||||
5. Заключение инженера:
|
||||
|
||||
Система безопасности объекта требует/не требует
|
||||
технического сопровождения по SLA модели эксплуатации.
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.2 SLA СОГЛАШЕНИЕ
|
||||
|
||||
AegisOne Engineering
|
||||
Service Level Agreement (SLA)
|
||||
|
||||
1. Объект обслуживания
|
||||
2. Перечень систем
|
||||
3. Время реакции:
|
||||
P1 — 2 часа
|
||||
P2 — 4 часа
|
||||
P3 — 24 часа
|
||||
|
||||
4. Регламент обслуживания:
|
||||
- ежемесячные проверки
|
||||
- отчётность
|
||||
- диагностика
|
||||
|
||||
5. Зоны ответственности:
|
||||
- заказчик
|
||||
- инженерная компания
|
||||
|
||||
6. Исключения:
|
||||
- повреждение третьими лицами
|
||||
- форс-мажор
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.3 АКТ ТЕХНИЧЕСКОГО АУДИТА
|
||||
|
||||
Проверено:
|
||||
- видеонаблюдение
|
||||
- СКУД
|
||||
- пожарная система
|
||||
|
||||
Результаты:
|
||||
- __________
|
||||
|
||||
Рекомендации:
|
||||
- критические
|
||||
- важные
|
||||
- плановые
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.4 ПАСПОРТ ОБЪЕКТА
|
||||
|
||||
AegisOne Engineering
|
||||
Паспорт инженерных систем объекта
|
||||
|
||||
Содержит:
|
||||
- схема систем
|
||||
- оборудование
|
||||
- IP-адреса
|
||||
- точки отказа
|
||||
- история обслуживания
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.5 ОТЧЁТ ПО ИНЦИДЕНТУ
|
||||
|
||||
Описание инцидента:
|
||||
Время реакции:
|
||||
Причина:
|
||||
Последствия:
|
||||
Устранение:
|
||||
Рекомендации:
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. СЦЕНАРИИ ВЗАИМОДЕЙСТВИЯ
|
||||
|
||||
|
||||
---
|
||||
|
||||
СЦЕНАРИЙ 1 — ВХОД
|
||||
|
||||
Контакт → мини-диагностика → аудит → отчет → SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
СЦЕНАРИЙ 2 — АВАРИЯ
|
||||
|
||||
Инцидент → выезд → устранение → отчет → предложение SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
СЦЕНАРИЙ 3 — ПАРТНЁРСКИЙ ВХОД
|
||||
|
||||
Партнёр → объект → аудит → сопровождение
|
||||
|
||||
|
||||
---
|
||||
|
||||
СЦЕНАРИЙ 4 — ТЕНДЕР / КОММЕРЧЕСКИЙ ВХОД
|
||||
|
||||
Запрос → аудит → КП → SLA контракт
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. КАК ЭТО ВОСПРИНИМАЕТСЯ РЫНКОМ
|
||||
|
||||
Ты становишься не:
|
||||
|
||||
❌ монтажной фирмой
|
||||
❌ сервисниками
|
||||
❌ подрядчиком
|
||||
|
||||
|
||||
---
|
||||
|
||||
А:
|
||||
|
||||
🧠 инженерной эксплуатационной системой объектов
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. ГЛАВНЫЙ ЭФФЕКТ ЭТОЙ УПАКОВКИ
|
||||
|
||||
Клиент думает:
|
||||
|
||||
> “Это не монтажники. Это те, кто контролирует безопасность объекта как систему.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. СЛЕДУЮЩИЙ УРОВЕНЬ (ЕСЛИ ПРОДОЛЖАЕМ)
|
||||
|
||||
Я могу собрать тебе дальше:
|
||||
|
||||
автоматическую CRM (Notion / 1C модель под эти разделы)
|
||||
|
||||
шаблон коммерческого предложения под каждый блок
|
||||
|
||||
визуальную архитектуру сайта (UI/UX)
|
||||
|
||||
систему KPI инженеров
|
||||
|
||||
модель масштабирования до федерального уровня
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ИТОГ
|
||||
|
||||
Ты только что получил:
|
||||
|
||||
> полноценную архитектуру инженерной компании уровня enterprise
|
||||
|
||||
|
||||
|
||||
И если это внедрить — ты перестаёшь быть “подрядчиком”.
|
||||
|
||||
Ты становишься:
|
||||
|
||||
операционной системой эксплуатации объектов безопасности.
|
||||
@@ -0,0 +1,543 @@
|
||||
Хороший вопрос — блог в твоей нише (системы безопасности + SLA + B2B + обслуживание) это не “контент-маркетинг”, а:
|
||||
|
||||
> инструмент продажи доверия, SLA и инженерной компетентности
|
||||
|
||||
|
||||
|
||||
Если блог сделан правильно, он заменяет:
|
||||
|
||||
отдел продаж (частично)
|
||||
|
||||
коммерческие предложения (частично)
|
||||
|
||||
презентации
|
||||
|
||||
кейсы
|
||||
|
||||
и даже тендерные обоснования
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. ГЛАВНАЯ ОШИБКА БОЛЬШИНСТВА БЛОГОВ В ТВОЕЙ НИШЕ
|
||||
|
||||
Они делают:
|
||||
|
||||
“мы установили систему”
|
||||
|
||||
“мы молодцы”
|
||||
|
||||
“как выбрать камеру”
|
||||
|
||||
“новости компании”
|
||||
|
||||
|
||||
👉 директору (заказчику B2B) это НЕ интересно
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. ЧТО ХОЧЕТ ВИДЕТЬ ДИРЕКТОР (ЗАКАЗЧИК B2B)
|
||||
|
||||
Директору не нужен контент.
|
||||
|
||||
Ему нужно:
|
||||
|
||||
1. Снижение риска
|
||||
|
||||
“у меня не сломается объект”
|
||||
|
||||
“меня не оштрафуют”
|
||||
|
||||
“у меня будет доказательная база”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. Управляемость
|
||||
|
||||
SLA
|
||||
|
||||
контроль
|
||||
|
||||
прозрачность
|
||||
|
||||
ответственность
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. Деньги и потери
|
||||
|
||||
сколько стоит простой
|
||||
|
||||
сколько стоит отказ системы
|
||||
|
||||
где риски потерь
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. Компетентность подрядчика
|
||||
|
||||
понимает ли подрядчик реальные риски
|
||||
|
||||
умеет ли он управлять системой, а не “чинить камеры”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. КАК ДОЛЖЕН БЫТЬ УСТРОЕН БЛОГ (АРХИТЕКТУРА)
|
||||
|
||||
BLOG AEGISONE
|
||||
|
||||
1. Risk Engineering (риски объектов)
|
||||
2. SLA & Service (обслуживание)
|
||||
3. Incident Cases (разбор аварий)
|
||||
4. Infrastructure Deep Dive (техническая экспертиза)
|
||||
5. Compliance / MCHS / Audit (регуляторика)
|
||||
6. Economics of Security (экономика потерь)
|
||||
7. Case Studies (реальные объекты)
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. КАТЕГОРИИ БЛОГА (ПОЛНАЯ СТРУКТУРА)
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.1 RISK ENGINEERING (САМАЯ ВАЖНАЯ)
|
||||
|
||||
Суть:
|
||||
|
||||
Показывает директору:
|
||||
|
||||
> где он уже теряет деньги, даже если всё “работает”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Примеры статей:
|
||||
|
||||
“Почему 70% объектов теряют видеодоказательства и не знают об этом”
|
||||
|
||||
“Что происходит, когда SLA пожарной системы формальный”
|
||||
|
||||
“7 скрытых точек отказа в СКУД”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формат:
|
||||
|
||||
риск
|
||||
|
||||
последствия
|
||||
|
||||
реальные сценарии
|
||||
|
||||
стоимость потерь
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Почему это продаёт:
|
||||
|
||||
👉 вызывает страх + осознание потерь
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.2 SLA & SERVICE (ТВОЯ ДЕНЕЖНАЯ КАТЕГОРИЯ)
|
||||
|
||||
Суть:
|
||||
|
||||
объясняет, почему SLA = бизнес-стабильность
|
||||
|
||||
|
||||
---
|
||||
|
||||
Примеры:
|
||||
|
||||
“Почему обслуживание пожарной сигнализации — это не обслуживание, а юридическая ответственность”
|
||||
|
||||
“Как SLA снижает риск остановки бизнеса”
|
||||
|
||||
“Почему разовые выезды не работают”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формат:
|
||||
|
||||
объяснение SLA
|
||||
|
||||
цифры
|
||||
|
||||
последствия без SLA
|
||||
|
||||
кейсы
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.3 INCIDENT CASES (СИЛЬНЕЙШИЙ ДОВЕРИТЕЛЬНЫЙ БЛОК)
|
||||
|
||||
Суть:
|
||||
|
||||
реальные аварии
|
||||
|
||||
|
||||
---
|
||||
|
||||
Примеры:
|
||||
|
||||
“Как один отказ СКУД остановил склад на 14 часов”
|
||||
|
||||
“Пожарная система без регламента: разбор инцидента”
|
||||
|
||||
“Почему не работал архив видеонаблюдения 3 месяца”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Структура:
|
||||
|
||||
1. Что случилось
|
||||
2. Почему это произошло
|
||||
3. Какие были последствия
|
||||
4. Как это выявили
|
||||
5. Как исправили
|
||||
6. Как избежать
|
||||
|
||||
|
||||
---
|
||||
|
||||
Это:
|
||||
|
||||
👉 главный доверительный инструмент
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.4 INFRASTRUCTURE DEEP DIVE
|
||||
|
||||
Суть:
|
||||
|
||||
показывает инженерную глубину
|
||||
|
||||
|
||||
---
|
||||
|
||||
Примеры:
|
||||
|
||||
“Как устроена современная система видеонаблюдения на объекте 10 000 м²”
|
||||
|
||||
“Почему сеть — это главный риск безопасности”
|
||||
|
||||
“UPS как критический элемент безопасности”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.5 COMPLIANCE / MCHS
|
||||
|
||||
Суть:
|
||||
|
||||
работа с нормативкой
|
||||
|
||||
|
||||
---
|
||||
|
||||
Примеры:
|
||||
|
||||
“Что проверяет МЧС в 2026 году”
|
||||
|
||||
“Типовые ошибки объектов при проверках”
|
||||
|
||||
“Почему формальное ТО приводит к штрафам”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.6 ECONOMICS OF SECURITY
|
||||
|
||||
Суть:
|
||||
|
||||
деньги, потери, риск
|
||||
|
||||
|
||||
---
|
||||
|
||||
Примеры:
|
||||
|
||||
“Сколько стоит 1 час простоя склада”
|
||||
|
||||
“Стоимость потери видеодоказательства”
|
||||
|
||||
“Почему дешёвый подрядчик обходится дороже”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.7 CASE STUDIES (ОСНОВА ПРОДАЖ)
|
||||
|
||||
Суть:
|
||||
|
||||
твои реальные объекты
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формат:
|
||||
|
||||
объект
|
||||
|
||||
проблемы
|
||||
|
||||
что сделали
|
||||
|
||||
результат
|
||||
|
||||
SLA модель
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. КАК ДОЛЖЕН ВЫГЛЯДЕТЬ БЛОГ (UI/UX)
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.1 НЕ ДОЛЖНО БЫТЬ:
|
||||
|
||||
❌ “новости компании”
|
||||
❌ “мы молодцы”
|
||||
❌ маркетинговых текстов
|
||||
❌ воды
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.2 ДОЛЖНО БЫТЬ:
|
||||
|
||||
Главная страница блога:
|
||||
|
||||
“инженерные риски”
|
||||
|
||||
“разбор инцидентов”
|
||||
|
||||
“стоимость отказов”
|
||||
|
||||
“кейсы объектов”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.3 Визуально:
|
||||
|
||||
строгий технический стиль
|
||||
|
||||
таблицы
|
||||
|
||||
схемы
|
||||
|
||||
диаграммы
|
||||
|
||||
риск-блоки
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. КАЖДАЯ СТАТЬЯ ДОЛЖНА ПРОДАВАТЬ
|
||||
|
||||
|
||||
---
|
||||
|
||||
структура статьи (обязательная):
|
||||
|
||||
1. Проблема
|
||||
2. Реальный сценарий
|
||||
3. Что происходит технически
|
||||
4. Риски
|
||||
5. Финансовые последствия
|
||||
6. Как решается
|
||||
7. Как это делает AegisOne
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. ПРИМЕР “ПРОДАЮЩЕЙ” СТАТЬИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Заголовок:
|
||||
|
||||
> “Почему 80% систем видеонаблюдения не дают доказательств в момент инцидента”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Внутри:
|
||||
|
||||
потеря архива
|
||||
|
||||
сбои HDD
|
||||
|
||||
неправильная настройка
|
||||
|
||||
отсутствие контроля
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Финал:
|
||||
|
||||
> “в AegisOne это решается через SLA мониторинг и инженерный контроль”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. КАК БЛОГ ПРИВОДИТ КЛИЕНТА
|
||||
|
||||
|
||||
---
|
||||
|
||||
Статья → Осознание риска → Страх потерь → Аудит → SLA контракт
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. ГЛАВНАЯ ЛОГИКА БЛОГА
|
||||
|
||||
Ты не пишешь статьи.
|
||||
|
||||
Ты создаёшь:
|
||||
|
||||
> систему формирования потребности в SLA
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
10. ЧТО ДЕЛАЕТ БЛОГ СИЛЬНЫМ (КРИТИЧНО)
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. РЕАЛЬНЫЕ ЦИФРЫ
|
||||
|
||||
потери
|
||||
|
||||
простои
|
||||
|
||||
риски
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. РЕАЛЬНЫЕ ИНЦИДЕНТЫ
|
||||
|
||||
аварии
|
||||
|
||||
сбои
|
||||
|
||||
последствия
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. ИНЖЕНЕРНАЯ ГЛУБИНА
|
||||
|
||||
схемы
|
||||
|
||||
логика систем
|
||||
|
||||
причины отказов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ЭКОНОМИКА
|
||||
|
||||
сколько стоит ошибка
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
11. ЧТО НЕЛЬЗЯ ДЕЛАТЬ
|
||||
|
||||
|
||||
---
|
||||
|
||||
❌ SEO-статьи “для трафика”
|
||||
❌ поверхностные обзоры
|
||||
❌ “лайфхаки безопасности”
|
||||
❌ маркетинг
|
||||
|
||||
|
||||
---
|
||||
|
||||
12. ИТОГ
|
||||
|
||||
Правильный блог в твоей модели — это:
|
||||
|
||||
> “система инженерного страха + доверия + экспертизы”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
13. ЕСЛИ РАЗВИВАТЬ ДАЛЬШЕ
|
||||
|
||||
Я могу дальше собрать:
|
||||
|
||||
30 готовых статей под твой блог (с заголовками + структурами)
|
||||
|
||||
SEO-структуру под Краснодарский край
|
||||
|
||||
Telegram-воронку из блога
|
||||
|
||||
автоматическое превращение статей в заявки на аудит
|
||||
|
||||
систему “контент → SHS влияние”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Если коротко:
|
||||
|
||||
👉 блог в твоей модели — это не контент
|
||||
👉 это “машина продажи SLA через понимание риска”
|
||||
@@ -0,0 +1,445 @@
|
||||
Ок, здесь мы уже собираем не “маркетинг”, а полноценную B2B-машину:
|
||||
|
||||
# вход → доверие → аудит → SLA → долгий повторяющийся доход
|
||||
|
||||
Ты как раз в нише, где деньги делаются не на разовых монтажах, а на:
|
||||
|
||||
> эксплуатации + рисках + ответственности
|
||||
|
||||
---
|
||||
|
||||
# ЧАСТЬ 1. ПЕРВАЯ ВОРОНКА КЛИЕНТОВ (КРАСНОДАРСКИЙ КРАЙ)
|
||||
|
||||
---
|
||||
|
||||
# 0. ЛОГИКА ВОРОНКИ
|
||||
|
||||
Твоя воронка НЕ классическая:
|
||||
|
||||
❌ “реклама → заявка → продажа”
|
||||
|
||||
У тебя:
|
||||
|
||||
# контент → доверие → аудит → выявление проблем → SLA
|
||||
|
||||
---
|
||||
|
||||
# 1. ВХОД В ВОРОНКУ (ТОЧКИ ПРИКОСНОВЕНИЯ)
|
||||
|
||||
## 1.1. Локальный B2B рынок (основа)
|
||||
|
||||
Ты работаешь не по всей РФ, а:
|
||||
|
||||
- Краснодар
|
||||
- Сочи
|
||||
- Новороссийск
|
||||
- Анапа
|
||||
- Армавир
|
||||
- промзоны + логистика + гостиницы
|
||||
|
||||
---
|
||||
|
||||
## 1.2. Каналы входа (без агрессивного маркетинга)
|
||||
|
||||
### 1) Telegram (основной канал доверия)
|
||||
|
||||
Контент + кейсы + разборы
|
||||
|
||||
---
|
||||
|
||||
### 2) Яндекс / Google SEO
|
||||
|
||||
Страницы:
|
||||
|
||||
- аудит систем безопасности
|
||||
- SLA обслуживание
|
||||
- обслуживание видеонаблюдения Краснодар
|
||||
- обслуживание пожарной сигнализации
|
||||
|
||||
---
|
||||
|
||||
### 3) Партнеры (очень важно)
|
||||
|
||||
- электрики
|
||||
- IT-аутсорс
|
||||
- управляющие компании
|
||||
- проектировщики
|
||||
- пожарники
|
||||
- строители
|
||||
|
||||
---
|
||||
|
||||
### 4) Прямые инженерные выходы
|
||||
|
||||
НЕ продажи.
|
||||
|
||||
Формат:
|
||||
|
||||
> “если нужен технический аудит или проверка систем — можем подключиться”
|
||||
|
||||
---
|
||||
|
||||
# 2. ПЕРВЫЙ ПРОДУКТ (ВХОД В ДЕНЬГИ)
|
||||
|
||||
# 🔥 Технический аудит объекта
|
||||
|
||||
---
|
||||
|
||||
## Почему это ядро воронки
|
||||
|
||||
Потому что:
|
||||
|
||||
- легко продать без давления
|
||||
- низкий чек
|
||||
- высокая ценность
|
||||
- выявляет проблемы
|
||||
- логично ведёт в SLA
|
||||
|
||||
---
|
||||
|
||||
## Цена (Краснодарский край)
|
||||
|
||||
- малый объект: 15–25 тыс
|
||||
- средний: 25–50 тыс
|
||||
- крупный: 50–100 тыс
|
||||
|
||||
---
|
||||
|
||||
## Что важно
|
||||
|
||||
Аудит НЕ продаёт услуги.
|
||||
|
||||
Он:
|
||||
|
||||
# показывает проблемы → создаёт необходимость SLA
|
||||
|
||||
---
|
||||
|
||||
# 3. ВТОРОЙ ЭТАП
|
||||
|
||||
# 🔥 “Диагностика + отчет”
|
||||
|
||||
После аудита:
|
||||
|
||||
Ты выдаешь документ:
|
||||
|
||||
- риски
|
||||
- неисправности
|
||||
- слабые места
|
||||
- последствия
|
||||
- рекомендации
|
||||
|
||||
---
|
||||
|
||||
## Ключевая мысль отчета:
|
||||
|
||||
> “Система формально работает, но не защищает объект”
|
||||
|
||||
---
|
||||
|
||||
# 4. ТРЕТИЙ ЭТАП
|
||||
|
||||
# 🔥 ПЕРЕХОД В SLA
|
||||
|
||||
---
|
||||
|
||||
## Это главный момент денег
|
||||
|
||||
Ты НЕ продаешь обслуживание.
|
||||
|
||||
Ты продаешь:
|
||||
|
||||
# “устранение рисков эксплуатации”
|
||||
|
||||
---
|
||||
|
||||
## Как выглядит оффер
|
||||
|
||||
> Мы берем объект на техническое сопровождение с SLA контролем состояния систем безопасности.
|
||||
|
||||
---
|
||||
|
||||
## Конверсия
|
||||
|
||||
Из аудита → в SLA:
|
||||
|
||||
- 30% слабые объекты
|
||||
- 50% средние
|
||||
- 10–20% сильные (очень хорошие деньги)
|
||||
|
||||
---
|
||||
|
||||
# 5. SLA (ПРОДУКТ ДЕНЕГ)
|
||||
|
||||
---
|
||||
|
||||
## Структура:
|
||||
|
||||
### START
|
||||
|
||||
25–40k
|
||||
|
||||
---
|
||||
|
||||
### BUSINESS
|
||||
|
||||
60–120k
|
||||
|
||||
---
|
||||
|
||||
### ENTERPRISE
|
||||
|
||||
150–400k
|
||||
|
||||
---
|
||||
|
||||
## Что ты продаешь на самом деле:
|
||||
|
||||
НЕ обслуживание
|
||||
|
||||
А:
|
||||
|
||||
# “инженерную стабильность объекта”
|
||||
|
||||
---
|
||||
|
||||
# 6. ПОЛНАЯ ВОРОНКА В ВИДЕ СХЕМЫ
|
||||
|
||||
```
|
||||
Контент / SEO / партнеры
|
||||
↓
|
||||
Лид (запрос)
|
||||
↓
|
||||
Мини-аудит (или первичная диагностика)
|
||||
↓
|
||||
Полный технический аудит
|
||||
↓
|
||||
Отчет с рисками
|
||||
↓
|
||||
SLA предложение
|
||||
↓
|
||||
Долгосрочный контракт
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# ЧАСТЬ 2. МОДЕЛЬ РОСТА ДО 2–3 МЛН ₽/МЕС RECURRENT
|
||||
|
||||
---
|
||||
|
||||
# 1. КЛЮЧЕВОЕ ПРАВИЛО
|
||||
|
||||
Ты не масштабируешь монтаж.
|
||||
|
||||
Ты масштабируешь:
|
||||
|
||||
# количество объектов на SLA
|
||||
|
||||
---
|
||||
|
||||
# 2. МАТЕМАТИКА МОДЕЛИ
|
||||
|
||||
---
|
||||
|
||||
## Вариант реалистичный (Краснодар → край → РФ)
|
||||
|
||||
### Средний чек SLA:
|
||||
|
||||
80 000 ₽
|
||||
|
||||
---
|
||||
|
||||
## Тебе нужно:
|
||||
|
||||
### 25–35 объектов
|
||||
|
||||
---
|
||||
|
||||
## Доход:
|
||||
|
||||
```
|
||||
30 объектов × 80 000 ₽ = 2 400 000 ₽/мес
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3. КАК ЭТО ДОСТИГАЕТСЯ
|
||||
|
||||
---
|
||||
|
||||
## ЭТАП 1 — 0–500k
|
||||
|
||||
Фокус:
|
||||
|
||||
- аудит
|
||||
- первые SLA
|
||||
- 5–7 объектов
|
||||
|
||||
---
|
||||
|
||||
## ЭТАП 2 — 500k–1.5M
|
||||
|
||||
Фокус:
|
||||
|
||||
- стабильные SLA
|
||||
- отбор клиентов
|
||||
- отказ от токсичных объектов
|
||||
- первые крупные клиенты
|
||||
|
||||
---
|
||||
|
||||
## ЭТАП 3 — 1.5M–3M
|
||||
|
||||
Фокус:
|
||||
|
||||
- стандартизация
|
||||
- регламенты
|
||||
- техподдержка
|
||||
- 2–3 инженера
|
||||
- удаленный контроль
|
||||
|
||||
---
|
||||
|
||||
# 4. КЛЮЧЕВОЙ РЫЧАГ РОСТА
|
||||
|
||||
---
|
||||
|
||||
# НЕ реклама
|
||||
|
||||
# НЕ продажи
|
||||
|
||||
# А:
|
||||
|
||||
## “плотность объектов на инженера”
|
||||
|
||||
---
|
||||
|
||||
# 5. СИСТЕМА РОСТА
|
||||
|
||||
---
|
||||
|
||||
## 1 инженер = 8–12 объектов
|
||||
|
||||
---
|
||||
|
||||
## значит:
|
||||
|
||||
|Кол-во инженеров|Доход|
|
||||
|---|---|
|
||||
|1|0.8–1.2M|
|
||||
|2|1.6–2.4M|
|
||||
|3|2.4–3.6M|
|
||||
|
||||
---
|
||||
|
||||
# 6. ГЛАВНЫЕ РЫЧАГИ МАСШТАБА
|
||||
|
||||
---
|
||||
|
||||
## 1. Аудит как продукт входа
|
||||
|
||||
(дешево / легко / продается сам)
|
||||
|
||||
---
|
||||
|
||||
## 2. SLA как подписка
|
||||
|
||||
(деньги каждый месяц)
|
||||
|
||||
---
|
||||
|
||||
## 3. Контент как доверие
|
||||
|
||||
(заменяет рекламу)
|
||||
|
||||
---
|
||||
|
||||
## 4. Партнерки
|
||||
|
||||
(дают теплые объекты)
|
||||
|
||||
---
|
||||
|
||||
## 5. Отказ от дешевых клиентов
|
||||
|
||||
(самый важный фактор)
|
||||
|
||||
---
|
||||
|
||||
# 7. КРИТИЧЕСКИЙ МОМЕНТ
|
||||
|
||||
---
|
||||
|
||||
Если ты не сделаешь это:
|
||||
|
||||
> “мы не берем монтаж без SLA”
|
||||
|
||||
ты останешься в рынке:
|
||||
|
||||
- демпинга
|
||||
- разовых работ
|
||||
- кассовых разрывов
|
||||
|
||||
---
|
||||
|
||||
# 8. ИДЕАЛЬНАЯ ЭВОЛЮЦИЯ БИЗНЕСА
|
||||
|
||||
---
|
||||
|
||||
## Было:
|
||||
|
||||
монтажники
|
||||
|
||||
---
|
||||
|
||||
## Стало:
|
||||
|
||||
SLA + эксплуатация
|
||||
|
||||
---
|
||||
|
||||
## Будет:
|
||||
|
||||
# внешний инженерный отдел объектов
|
||||
|
||||
---
|
||||
|
||||
# 9. СИЛЬНЫЙ СДВИГ МЫШЛЕНИЯ
|
||||
|
||||
---
|
||||
|
||||
Ты больше не продаешь:
|
||||
|
||||
❌ камеры
|
||||
❌ монтаж
|
||||
❌ оборудование
|
||||
|
||||
---
|
||||
|
||||
Ты продаешь:
|
||||
|
||||
# “спокойствие собственника за техническую инфраструктуру объекта”
|
||||
|
||||
---
|
||||
|
||||
# ЕСЛИ СЖАТЬ В ОДНУ ФОРМУЛУ:
|
||||
|
||||
---
|
||||
|
||||
> Аудит → выявление рисков → SLA → долгосрочный инженерный доход
|
||||
|
||||
---
|
||||
|
||||
# ЕСЛИ ДАЛЬШЕ ДВИГАТЬСЯ
|
||||
|
||||
Я могу следующим шагом собрать тебе:
|
||||
|
||||
- точную систему продаж аудита (скрипты + сообщения)
|
||||
- упаковку Telegram как лидогенератора
|
||||
- CRM воронку под SLA
|
||||
- регламент инженера (чтобы масштабироваться)
|
||||
- финансовую модель до 5–10 млн/мес
|
||||
|
||||
Это уже будет не “бизнес идея”, а:
|
||||
|
||||
# операционная система инженерной компании.
|
||||
@@ -0,0 +1,397 @@
|
||||
Отлично, это уже уровень, где бизнес начинает “дышать сам”:
|
||||
Telegram → доверие → аудит → SLA → регулярный доход.
|
||||
|
||||
Разобьём на две системы:
|
||||
|
||||
# 1) Telegram как лидогенератор (не блог, а воронка)
|
||||
|
||||
# 2) CRM-воронка под SLA (повторяемые деньги)
|
||||
|
||||
---
|
||||
|
||||
# 1. TELEGRAM КАК ЛИДОГЕНЕРАТОР (ИНЖЕНЕРНАЯ МОДЕЛЬ)
|
||||
|
||||
---
|
||||
|
||||
## 1.1. Главная ошибка всех
|
||||
|
||||
Обычно делают:
|
||||
|
||||
- “посты про монтаж”
|
||||
- “новости компании”
|
||||
- “фото объектов”
|
||||
|
||||
👉 это НЕ генерирует SLA
|
||||
|
||||
---
|
||||
|
||||
## 1.2. Твоя роль канала
|
||||
|
||||
Твой Telegram — это:
|
||||
|
||||
# “инженерный журнал рисков объектов”
|
||||
|
||||
Не маркетинг. Не реклама. Не блог.
|
||||
|
||||
---
|
||||
|
||||
## 1.3. Главная цель Telegram
|
||||
|
||||
Не подписки.
|
||||
|
||||
А:
|
||||
|
||||
> заявки на аудит
|
||||
|
||||
---
|
||||
|
||||
# 2. СТРУКТУРА TELEGRAM-КАНАЛА
|
||||
|
||||
---
|
||||
|
||||
## 2.1. Закреп (ОБЯЗАТЕЛЬНО)
|
||||
|
||||
```text
|
||||
Мы занимаемся инженерной эксплуатацией систем безопасности коммерческих объектов.
|
||||
|
||||
Перед обслуживанием всегда проводим технический аудит:
|
||||
— видеонаблюдение
|
||||
— СКУД
|
||||
— пожарная сигнализация
|
||||
— инфраструктура
|
||||
|
||||
Цель — выявить скрытые риски, которые не видны в ежедневной работе.
|
||||
|
||||
📌 Если нужно — можем провести аудит объекта и выдать инженерное заключение.
|
||||
|
||||
Написать: @your_contact
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2.2. 3 типа контента (ВСЕГДА)
|
||||
|
||||
---
|
||||
|
||||
### 1) РИСКИ (60%)
|
||||
|
||||
👉 основа роста
|
||||
|
||||
Примеры:
|
||||
|
||||
- “Почему архив видеонаблюдения исчезает незаметно”
|
||||
- “Почему пожарка работает, но не защищает”
|
||||
- “3 скрытые ошибки СКУД”
|
||||
|
||||
---
|
||||
|
||||
### 2) КЕЙСЫ (25%)
|
||||
|
||||
👉 доверие
|
||||
|
||||
Примеры:
|
||||
|
||||
- “Аудит склада: 12 критических ошибок”
|
||||
- “Гостиница без резервного питания”
|
||||
|
||||
---
|
||||
|
||||
### 3) ЭКСПЕРТИЗА (15%)
|
||||
|
||||
👉 статус
|
||||
|
||||
Примеры:
|
||||
|
||||
- “Как должен выглядеть нормальный SLA”
|
||||
- “Что проверяет инженер, а не монтажник”
|
||||
|
||||
---
|
||||
|
||||
# 2.3. ФОРМАТ ПОСТОВ (СТАНДАРТ)
|
||||
|
||||
```text
|
||||
1. Проблема
|
||||
2. Инженерное объяснение
|
||||
3. Последствия для бизнеса
|
||||
4. Спокойный вывод
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Пример готового поста
|
||||
|
||||
```text
|
||||
Большинство объектов уверены, что видеонаблюдение работает корректно.
|
||||
|
||||
На практике часто есть скрытая проблема — архив.
|
||||
|
||||
Камеры могут показывать картинку, но запись:
|
||||
— не сохраняется полностью
|
||||
— перезаписывается раньше срока
|
||||
— теряет часть каналов
|
||||
— сбоит из-за питания или дисков
|
||||
|
||||
Это не видно в ежедневной работе.
|
||||
|
||||
Проблема проявляется только после инцидента.
|
||||
|
||||
По сути, система существует, но не выполняет свою функцию хранения доказательной базы.
|
||||
|
||||
Именно поэтому видеонаблюдение нужно рассматривать как систему эксплуатации, а не просто установленное оборудование.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 2.4. CTA (ОЧЕНЬ ВАЖНО)
|
||||
|
||||
Каждый 3–4 пост:
|
||||
|
||||
```text
|
||||
Если нужно — можем провести технический аудит объекта и проверить состояние систем безопасности.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 2.5. ЛИД-МАГНИТ (ОЧЕНЬ СИЛЬНЫЙ РЫЧАГ)
|
||||
|
||||
---
|
||||
|
||||
## PDF:
|
||||
|
||||
# “Чек-лист скрытых рисков систем безопасности”
|
||||
|
||||
или
|
||||
|
||||
# “10 ошибок эксплуатации объектов”
|
||||
|
||||
---
|
||||
|
||||
## Telegram пост:
|
||||
|
||||
```text
|
||||
Мы собрали чек-лист из 10 критических ошибок, которые встречаются на большинстве объектов.
|
||||
|
||||
Если хотите — отправим бесплатно.
|
||||
```
|
||||
|
||||
👉 это даёт первые контакты без давления
|
||||
|
||||
---
|
||||
|
||||
# 3. TELEGRAM → ВОРОНКА
|
||||
|
||||
```text
|
||||
Контент
|
||||
↓
|
||||
Интерес
|
||||
↓
|
||||
Чек-лист / диалог
|
||||
↓
|
||||
Диагностика (мини-аудит)
|
||||
↓
|
||||
Полный аудит
|
||||
↓
|
||||
SLA
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4. CRM-ВОРОНКА ПОД SLA
|
||||
|
||||
Теперь самое важное.
|
||||
|
||||
Ты не продаёшь “услуги”. Ты ведёшь объекты по состояниям.
|
||||
|
||||
---
|
||||
|
||||
# 4.1. СТРУКТУРА CRM (СТАДИИ)
|
||||
|
||||
---
|
||||
|
||||
## 1. ЛИД
|
||||
|
||||
Источник:
|
||||
|
||||
- Telegram
|
||||
- сайт
|
||||
- партнер
|
||||
- холодный контакт
|
||||
|
||||
---
|
||||
|
||||
## 2. ПЕРВИЧНЫЙ КОНТАКТ
|
||||
|
||||
- написал / ответил
|
||||
- уточнение объекта
|
||||
|
||||
---
|
||||
|
||||
## 3. КВАЛИФИКАЦИЯ
|
||||
|
||||
Вопросы:
|
||||
|
||||
- тип объекта
|
||||
- системы
|
||||
- есть ли подрядчик
|
||||
- проблемы
|
||||
|
||||
---
|
||||
|
||||
## 4. ДИАГНОСТИКА (мини-аудит)
|
||||
|
||||
- быстрый осмотр
|
||||
- удаленно или выезд
|
||||
- выявление боли
|
||||
|
||||
---
|
||||
|
||||
## 5. ПОЛНЫЙ АУДИТ
|
||||
|
||||
- инженерный выезд
|
||||
- отчет
|
||||
- риски
|
||||
|
||||
---
|
||||
|
||||
## 6. ПРЕДЛОЖЕНИЕ SLA
|
||||
|
||||
- тариф
|
||||
- регламент
|
||||
- реакция
|
||||
|
||||
---
|
||||
|
||||
## 7. ПЕРЕГОВОРЫ
|
||||
|
||||
- согласование цены
|
||||
- условия
|
||||
|
||||
---
|
||||
|
||||
## 8. ДОГОВОР SLA
|
||||
|
||||
- подписан
|
||||
- объект активен
|
||||
|
||||
---
|
||||
|
||||
## 9. АКТИВНЫЙ SLA
|
||||
|
||||
- обслуживание
|
||||
- отчёты
|
||||
|
||||
---
|
||||
|
||||
## 10. РАСШИРЕНИЕ
|
||||
|
||||
- новые объекты
|
||||
- доп. системы
|
||||
|
||||
---
|
||||
|
||||
# 4.2. ВИЗУАЛЬНАЯ CRM-СХЕМА
|
||||
|
||||
```text
|
||||
ЛИД
|
||||
↓
|
||||
КОНТАКТ
|
||||
↓
|
||||
КВАЛИФИКАЦИЯ
|
||||
↓
|
||||
МИНИ-ДИАГНОСТИКА
|
||||
↓
|
||||
АУДИТ
|
||||
↓
|
||||
SLA ОФФЕР
|
||||
↓
|
||||
ДОГОВОР
|
||||
↓
|
||||
ОБСЛУЖИВАНИЕ
|
||||
↓
|
||||
РАСШИРЕНИЕ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 5. ПРОСТАЯ CRM В ТАБЛИЦЕ (СТАРТ)
|
||||
|
||||
Если без софта:
|
||||
|
||||
|Клиент|Объект|Статус|Проблема|Этап|Потенциал|
|
||||
|---|---|---|---|---|---|
|
||||
|X|гостиница|аудит|нет архива|прогрев|80k/мес|
|
||||
|
||||
---
|
||||
|
||||
# 6. КЛЮЧЕВОЙ ПРИНЦИП CRM
|
||||
|
||||
Ты не ведёшь “продажи”.
|
||||
|
||||
Ты ведёшь:
|
||||
|
||||
# инженерные объекты
|
||||
|
||||
---
|
||||
|
||||
# 7. МЕТРИКА УСПЕХА (ВАЖНО)
|
||||
|
||||
Не:
|
||||
|
||||
- подписчики
|
||||
- заявки
|
||||
|
||||
А:
|
||||
|
||||
## 1 объект = SLA контракт
|
||||
|
||||
---
|
||||
|
||||
# 8. РОЛЬ TELEGRAM В МОДЕЛИ ДЕНЕГ
|
||||
|
||||
```text
|
||||
Telegram = генератор доверия
|
||||
Аудит = генератор денег входа
|
||||
SLA = генератор повторного дохода
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 9. СИСТЕМА РОСТА (СВЯЗКА ВСЕГО)
|
||||
|
||||
```text
|
||||
Контент (Telegram)
|
||||
↓
|
||||
Диалог
|
||||
↓
|
||||
Аудит
|
||||
↓
|
||||
SLA контракт
|
||||
↓
|
||||
Постоянный доход
|
||||
↓
|
||||
Масштаб через инженеров
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 10. СЛЕДУЮЩИЙ УРОВЕНЬ (если хочешь усилить систему)
|
||||
|
||||
Я могу дальше собрать тебе:
|
||||
|
||||
### 1. Полную CRM в Notion / 1C структуру под SLA
|
||||
|
||||
### 2. Скрипт “закрытия в SLA после аудита”
|
||||
|
||||
### 3. Таблицу квалификации клиента (кого брать/кого нет)
|
||||
|
||||
### 4. Финмодель роста до 3 млн+
|
||||
|
||||
### 5. Регламент инженера (чтобы масштабировать команду)
|
||||
|
||||
---
|
||||
|
||||
Если коротко: ты сейчас строишь не маркетинг, а:
|
||||
|
||||
> инженерную подписочную сервисную компанию
|
||||
|
||||
И Telegram + CRM — это её нервная система.
|
||||
@@ -0,0 +1,537 @@
|
||||
Ниже — полноценная модель **“Дашборд инженера (тимлид / руководитель техников)”** для AegisOne Engineering.
|
||||
|
||||
Это ключевой слой между:
|
||||
|
||||
- техник → (исполнение)
|
||||
- директор → (управление системой)
|
||||
|
||||
Инженер тут не “старший техник”, а:
|
||||
|
||||
> операционный контроллер SLA, качества и рисков на объектах
|
||||
|
||||
---
|
||||
|
||||
# 1. РОЛЬ ИНЖЕНЕРА В СИСТЕМЕ
|
||||
|
||||
## 1.1 Основная функция
|
||||
|
||||
```text
|
||||
Инженер = управление качеством исполнения SLA + распределение нагрузки + контроль рисков объектов
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.2 Он НЕ делает:
|
||||
|
||||
- не чинит оборудование (это техник)
|
||||
- не продаёт SLA (это коммерция)
|
||||
- не ведёт бухгалтерию
|
||||
|
||||
---
|
||||
|
||||
## 1.3 Он ДЕЛАЕТ:
|
||||
|
||||
- распределяет заявки
|
||||
- контролирует SLA
|
||||
- проверяет качество работ
|
||||
- снижает инциденты
|
||||
- управляет техниками
|
||||
- влияет на SHS
|
||||
|
||||
---
|
||||
|
||||
# 2. АРХИТЕКТУРА DASHBOARD ИНЖЕНЕРА
|
||||
|
||||
```text
|
||||
CEO DASHBOARD
|
||||
↓
|
||||
ENGINEER DASHBOARD
|
||||
↓
|
||||
TECHNICIAN DASHBOARD
|
||||
↓
|
||||
OBJECT SYSTEMS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3. DASHBOARD ИНЖЕНЕРА (REAL-TIME)
|
||||
|
||||
## 3.1 Главные блоки интерфейса
|
||||
|
||||
### Блок 1 — SLA контроль региона
|
||||
|
||||
- SLA compliance (%)
|
||||
- просроченные заявки
|
||||
- риск нарушения SLA
|
||||
- критические объекты
|
||||
|
||||
---
|
||||
|
||||
### Блок 2 — команда техников
|
||||
|
||||
- загрузка каждого техника
|
||||
- эффективность
|
||||
- ошибки / reopen rate
|
||||
- скорость реакции
|
||||
|
||||
---
|
||||
|
||||
### Блок 3 — инциденты
|
||||
|
||||
- P1 / P2 / P3
|
||||
- открытые критические
|
||||
- повторяющиеся аварии
|
||||
|
||||
---
|
||||
|
||||
### Блок 4 — объекты
|
||||
|
||||
- риск-уровень (Risk Score)
|
||||
- SHS влияние объекта
|
||||
- проблемные системы
|
||||
|
||||
---
|
||||
|
||||
### Блок 5 — KPI инженера
|
||||
|
||||
- качество управления SLA
|
||||
- распределение нагрузки
|
||||
- снижение инцидентов
|
||||
- эффективность команды
|
||||
|
||||
---
|
||||
|
||||
# 4. PHP АРХИТЕКТУРА
|
||||
|
||||
---
|
||||
|
||||
## 4.1 API инженера
|
||||
|
||||
```php
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=aegisone","user","pass");
|
||||
|
||||
$data = $pdo->query("
|
||||
SELECT
|
||||
e.id,
|
||||
e.name,
|
||||
|
||||
AVG(t.sla_compliance) as sla,
|
||||
COUNT(i.id) as incidents,
|
||||
AVG(t.response_time) as response_time
|
||||
|
||||
FROM engineers e
|
||||
LEFT JOIN tasks t ON t.engineer_id = e.id
|
||||
LEFT JOIN incidents i ON i.engineer_id = e.id
|
||||
|
||||
GROUP BY e.id
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4.2 SHS ENGINEER IMPACT API
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=aegisone","user","pass");
|
||||
|
||||
$k = $pdo->query("
|
||||
SELECT
|
||||
AVG(sla_compliance) as sla,
|
||||
AVG(response_time) as rt,
|
||||
AVG(reopen_rate) as rr
|
||||
FROM tasks
|
||||
")->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
/* Engineer Impact on SHS */
|
||||
$EIS =
|
||||
(0.5 * $k['sla']) +
|
||||
(0.3 * (1 - $k['rr'])) +
|
||||
(0.2 * (1 - $k['rt']/10));
|
||||
|
||||
echo json_encode([
|
||||
"ENGINEER_IMPACT_SCORE" => round($EIS * 100, 2)
|
||||
]);
|
||||
?>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 5. ФОРМУЛЫ ИНЖЕНЕРА (КРИТИЧЕСКИЕ KPI)
|
||||
|
||||
---
|
||||
|
||||
## 5.1 ENGINEER CONTROL SCORE (ECS)
|
||||
|
||||
```text
|
||||
ECS =
|
||||
(0.30 × SLA Compliance Control)
|
||||
+ (0.25 × Task Distribution Efficiency)
|
||||
+ (0.20 × Incident Reduction Rate)
|
||||
+ (0.15 × Team Performance Score)
|
||||
+ (0.10 × Response Coordination Speed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.2 SLA CONTROL INDEX
|
||||
|
||||
```text
|
||||
SCI = closed_tasks_in_SLA / total_tasks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.3 TEAM LOAD BALANCE
|
||||
|
||||
```text
|
||||
TLB = std_dev(tasks_per_engineer)
|
||||
```
|
||||
|
||||
👉 чем меньше → тем лучше распределение
|
||||
|
||||
---
|
||||
|
||||
## 5.4 INCIDENT REDUCTION RATE
|
||||
|
||||
```text
|
||||
IRR = (incidents_previous - incidents_current) / incidents_previous
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.5 ESCALATION RATE
|
||||
|
||||
```text
|
||||
ER = escalated_tasks / total_tasks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6. DASHBOARD (HTML + JS)
|
||||
|
||||
---
|
||||
|
||||
## 6.1 HTML
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Engineer Control Panel</title>
|
||||
|
||||
<style>
|
||||
body { background:#0f172a; color:white; font-family:Arial; }
|
||||
|
||||
.grid {
|
||||
display:grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap:15px;
|
||||
padding:20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background:#1e293b;
|
||||
padding:15px;
|
||||
border-radius:12px;
|
||||
}
|
||||
|
||||
.big { font-size:28px; font-weight:bold; }
|
||||
.green { color:#22c55e; }
|
||||
.yellow { color:#facc15; }
|
||||
.red { color:#ef4444; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h2 style="padding:20px;">Engineer Live Dashboard</h2>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<div class="card">
|
||||
<div>SLA Control</div>
|
||||
<div id="sla" class="big">--</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div>Engineer Impact (SHS)</div>
|
||||
<div id="shs" class="big">--</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div>Incidents</div>
|
||||
<div id="inc" class="big">--</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="team"></div>
|
||||
|
||||
<script src="engineer.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.2 JS realtime
|
||||
|
||||
```javascript
|
||||
async function loadEngineer(){
|
||||
|
||||
const res = await fetch('/api/engineer.php');
|
||||
const data = await res.json();
|
||||
|
||||
let sla = 0;
|
||||
let inc = 0;
|
||||
|
||||
let html = "";
|
||||
|
||||
data.forEach(e => {
|
||||
|
||||
sla += parseFloat(e.sla);
|
||||
inc += parseInt(e.incidents);
|
||||
|
||||
let color =
|
||||
e.sla > 0.9 ? "green" :
|
||||
e.sla > 0.75 ? "yellow" : "red";
|
||||
|
||||
html += `
|
||||
<div class="card">
|
||||
<h3>${e.name}</h3>
|
||||
|
||||
<p>SLA: <b class="${color}">
|
||||
${(e.sla * 100).toFixed(1)}%
|
||||
</b></p>
|
||||
|
||||
<p>Incidents: ${e.incidents}</p>
|
||||
<p>Response: ${e.response_time}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("team").innerHTML = html;
|
||||
|
||||
document.getElementById("sla").innerText =
|
||||
((sla / data.length) * 100).toFixed(1) + "%";
|
||||
|
||||
document.getElementById("inc").innerText = inc;
|
||||
|
||||
let shs = (sla / data.length) * 100 - inc * 0.5;
|
||||
|
||||
document.getElementById("shs").innerText =
|
||||
shs.toFixed(1);
|
||||
}
|
||||
|
||||
setInterval(loadEngineer, 15000);
|
||||
loadEngineer();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 7. ДОКУМЕНТЫ ИНЖЕНЕРА (ОБЯЗАТЕЛЬНЫЕ)
|
||||
|
||||
---
|
||||
|
||||
## 7.1 РЕГЛАМЕНТ ИНЖЕНЕРА
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Engineer Operational Standard
|
||||
|
||||
1. Назначение
|
||||
Инженер отвечает за выполнение SLA и контроль качества работ техников.
|
||||
|
||||
---
|
||||
|
||||
2. Обязанности
|
||||
|
||||
- распределение заявок
|
||||
- контроль SLA выполнения
|
||||
- контроль качества диагностики
|
||||
- управление инцидентами
|
||||
- предотвращение повторных аварий
|
||||
|
||||
---
|
||||
|
||||
3. Запрещено
|
||||
|
||||
- закрывать задачи без проверки
|
||||
- игнорировать SLA нарушения
|
||||
- делегировать без фиксации
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.2 ESCALATION MATRIX
|
||||
|
||||
```text
|
||||
P1 → инженер → директор (немедленно)
|
||||
P2 → инженер → инженер контроль
|
||||
P3 → техник
|
||||
|
||||
Если SLA риск > 80%:
|
||||
→ обязательная эскалация
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.3 ЧЕК-ЛИСТ ИНЖЕНЕРА
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Engineer Control Checklist
|
||||
|
||||
[ ] Все заявки распределены
|
||||
[ ] SLA риск оценён
|
||||
[ ] Техники назначены
|
||||
[ ] P1 инциденты закрыты
|
||||
[ ] Повторные аварии проанализированы
|
||||
[ ] Отчёты проверены
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.4 ОТЧЁТ ИНЖЕНЕРА
|
||||
|
||||
```text
|
||||
Engineer Report
|
||||
|
||||
Период:
|
||||
Инженер:
|
||||
|
||||
---
|
||||
|
||||
1. SLA состояние:
|
||||
-
|
||||
|
||||
2. Проблемные объекты:
|
||||
-
|
||||
|
||||
3. Инциденты:
|
||||
-
|
||||
|
||||
4. Перегруз команды:
|
||||
-
|
||||
|
||||
5. Принятые меры:
|
||||
-
|
||||
|
||||
6. Риски:
|
||||
-
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 8. ЧТО ИНЖЕНЕР ДОЛЖЕН ИМЕТЬ В СИСТЕМЕ
|
||||
|
||||
---
|
||||
|
||||
## ОБЯЗАТЕЛЬНО:
|
||||
|
||||
- SLA статус всех объектов
|
||||
- список техников
|
||||
- инциденты P1–P3
|
||||
- риск объектов
|
||||
- SHS влияние
|
||||
|
||||
---
|
||||
|
||||
## РЕКОМЕНДУЕМО:
|
||||
|
||||
- история объектов
|
||||
- схемы систем
|
||||
- фото оборудования
|
||||
- прошлые ремонты
|
||||
- нагрузка по часам
|
||||
|
||||
---
|
||||
|
||||
# 9. ЧТО ЕМУ НЕЛЬЗЯ ДАВАТЬ
|
||||
|
||||
---
|
||||
|
||||
## КАТЕГОРИЧЕСКИ НЕ НУЖНО:
|
||||
|
||||
- прямые продажи
|
||||
- финансовые данные прибыли
|
||||
- доступ к CRM сделкам
|
||||
- доступ к договорам без SLA слоя
|
||||
- административные функции директора
|
||||
|
||||
---
|
||||
|
||||
# 10. РИСКИ СИСТЕМЫ
|
||||
|
||||
---
|
||||
|
||||
## РИСК 1 — инженер превращается в диспетчера
|
||||
|
||||
👉 решение:
|
||||
|
||||
- оставить KPI, а не “ручное управление”
|
||||
|
||||
---
|
||||
|
||||
## РИСК 2 — перегруз инженера
|
||||
|
||||
👉 решение:
|
||||
|
||||
- TLB (load balance)
|
||||
|
||||
---
|
||||
|
||||
## РИСК 3 — потеря SLA контроля
|
||||
|
||||
👉 решение:
|
||||
|
||||
- SHS alert system
|
||||
|
||||
---
|
||||
|
||||
# 11. ГЛАВНАЯ ЛОГИКА
|
||||
|
||||
```text
|
||||
Техник → выполняет
|
||||
Инженер → управляет качеством
|
||||
Директор → управляет системой
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 12. ИТОГ
|
||||
|
||||
Ты получаешь:
|
||||
|
||||
> управляемый слой контроля SLA между исполнением и бизнесом
|
||||
|
||||
---
|
||||
|
||||
# 13. ЕСЛИ РАЗВИВАТЬ ДАЛЬШЕ
|
||||
|
||||
Я могу собрать:
|
||||
|
||||
- автораспределение заявок (AI dispatch)
|
||||
- прогноз поломок объектов
|
||||
- система “умного SLA риска”
|
||||
- мобильный кабинет инженера
|
||||
- интеграция с Telegram уведомлениями
|
||||
|
||||
---
|
||||
|
||||
Если коротко:
|
||||
|
||||
👉 это уже не “дашборд”
|
||||
👉 это уровень операционной системы инженерного бизнеса (mini-SAP под твою нишу)
|
||||
@@ -0,0 +1,424 @@
|
||||
Ниже — полноценная система **“Дашборд техника в реальном времени” + пакет документов и инструкций**, который превращает техника не в “исполнителя заявок”, а в **узел управляемой SLA-системы**.
|
||||
|
||||
Я разделю на 3 уровня:
|
||||
|
||||
1. Дашборд техника (web / PHP система)
|
||||
2. Документы и регламенты (полные тексты)
|
||||
3. Шаблоны (акты, чек-листы, отчёты)
|
||||
|
||||
---
|
||||
|
||||
# 1. DASHBOARD ТЕХНИКА (REAL-TIME)
|
||||
|
||||
## 1.1 Логика системы
|
||||
|
||||
```text
|
||||
Заявка → Назначение → Выезд → Диагностика → Исполнение → Закрытие → SLA фиксация → SHS влияние
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.2 Что должен видеть техник (главное)
|
||||
|
||||
### Блок 1 — текущая нагрузка
|
||||
|
||||
- активные заявки
|
||||
- срочность (P1–P3)
|
||||
- дедлайн SLA
|
||||
- расстояние до объекта
|
||||
- статус выполнения
|
||||
|
||||
---
|
||||
|
||||
### Блок 2 — SLA контроль
|
||||
|
||||
- осталось времени до нарушения SLA
|
||||
- критичность заявки
|
||||
- штрафной риск
|
||||
|
||||
---
|
||||
|
||||
### Блок 3 — оборудование объекта
|
||||
|
||||
- камеры / СКУД / пожарка
|
||||
- история неисправностей
|
||||
- последние ремонты
|
||||
|
||||
---
|
||||
|
||||
### Блок 4 — чек-лист работ
|
||||
|
||||
- пошаговый регламент
|
||||
- обязательные проверки
|
||||
- фотофиксация
|
||||
|
||||
---
|
||||
|
||||
### Блок 5 — результат
|
||||
|
||||
- закрытие заявки
|
||||
- причина
|
||||
- подтверждение клиента
|
||||
- влияние на KPI
|
||||
|
||||
---
|
||||
|
||||
# 2. PHP ДАШБОРД ТЕХНИКА (ГОТОВЫЙ ШАБЛОН)
|
||||
|
||||
---
|
||||
|
||||
## 2.1 API задач техника
|
||||
|
||||
```php
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=aegisone","user","pass");
|
||||
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
t.id,
|
||||
t.title,
|
||||
t.priority,
|
||||
t.status,
|
||||
t.deadline,
|
||||
t.address,
|
||||
t.system_type,
|
||||
t.sla_remaining_hours
|
||||
FROM tasks t
|
||||
WHERE t.status != 'closed'
|
||||
ORDER BY t.priority DESC, t.deadline ASC
|
||||
");
|
||||
|
||||
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
?>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2.2 FRONTEND (ТЕХНИК DASHBOARD)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Technician Panel - AegisOne</title>
|
||||
|
||||
<style>
|
||||
body { background:#0f172a; color:white; font-family:Arial; }
|
||||
.card { background:#1e293b; padding:15px; margin:10px; border-radius:10px; }
|
||||
.grid { display:grid; grid-template-columns: repeat(2, 1fr); gap:10px; }
|
||||
.red { color:#ef4444; }
|
||||
.yellow { color:#facc15; }
|
||||
.green { color:#22c55e; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h2 style="padding:10px;">Technician Live Dashboard</h2>
|
||||
|
||||
<div id="tasks"></div>
|
||||
|
||||
<script src="tech.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2.3 JS realtime обновление
|
||||
|
||||
```javascript
|
||||
async function loadTasks(){
|
||||
const res = await fetch('/api/tech_tasks.php');
|
||||
const data = await res.json();
|
||||
|
||||
let html = "";
|
||||
|
||||
data.forEach(t => {
|
||||
|
||||
let color =
|
||||
t.sla_remaining_hours < 2 ? "red" :
|
||||
t.sla_remaining_hours < 6 ? "yellow" : "green";
|
||||
|
||||
html += `
|
||||
<div class="card">
|
||||
<h3>${t.title}</h3>
|
||||
|
||||
<p><b>Priority:</b> ${t.priority}</p>
|
||||
<p><b>Address:</b> ${t.address}</p>
|
||||
<p><b>System:</b> ${t.system_type}</p>
|
||||
|
||||
<p><b>SLA:</b>
|
||||
<span class="${color}">
|
||||
${t.sla_remaining_hours}h left
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<button onclick="startTask(${t.id})">Start</button>
|
||||
<button onclick="closeTask(${t.id})">Close</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("tasks").innerHTML = html;
|
||||
}
|
||||
|
||||
setInterval(loadTasks, 10000);
|
||||
loadTasks();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3. ДОКУМЕНТЫ ДЛЯ ТЕХНИКА (ОБЯЗАТЕЛЬНЫЕ)
|
||||
|
||||
---
|
||||
|
||||
# 3.1 РЕГЛАМЕНТ РАБОТЫ ТЕХНИКА
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Регламент работы инженера-техника
|
||||
|
||||
1. Общие положения
|
||||
|
||||
Техник обязан выполнять работы строго по SLA и чек-листу объекта.
|
||||
Любое отклонение фиксируется в системе.
|
||||
|
||||
---
|
||||
|
||||
2. Прибытие на объект
|
||||
|
||||
- зарегистрировать прибытие в системе
|
||||
- подтвердить время начала работ
|
||||
- сделать фото объекта до начала работ
|
||||
|
||||
---
|
||||
|
||||
3. Диагностика
|
||||
|
||||
- проверить систему по чек-листу
|
||||
- зафиксировать неисправности
|
||||
- определить причину (если возможно)
|
||||
|
||||
---
|
||||
|
||||
4. Выполнение работ
|
||||
|
||||
- устранить неисправность
|
||||
- не изменять конфигурацию без согласования
|
||||
- использовать только разрешённые материалы
|
||||
|
||||
---
|
||||
|
||||
5. Завершение работ
|
||||
|
||||
- тестирование системы
|
||||
- фото после выполнения
|
||||
- подтверждение работоспособности
|
||||
|
||||
---
|
||||
|
||||
6. Закрытие заявки
|
||||
|
||||
- заполнить отчет
|
||||
- указать причину неисправности
|
||||
- получить подтверждение клиента (если возможно)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3.2 ЧЕК-ЛИСТ ВЫЕЗДА
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Checklist Technician Visit
|
||||
|
||||
[ ] Прибытие на объект зафиксировано
|
||||
[ ] Фото “до”
|
||||
[ ] Проверка питания системы
|
||||
[ ] Проверка камер / датчиков
|
||||
[ ] Проверка СКУД
|
||||
[ ] Проверка пожарной панели
|
||||
[ ] Локализация неисправности
|
||||
[ ] Устранение проблемы
|
||||
[ ] Тестирование системы
|
||||
[ ] Фото “после”
|
||||
[ ] Закрытие заявки
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3.3 СТАНДАРТ ДИАГНОСТИКИ
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Diagnostic Standard
|
||||
|
||||
1. Неисправность фиксируется только после проверки:
|
||||
- питания
|
||||
- сети
|
||||
- оборудования
|
||||
- конфигурации
|
||||
|
||||
2. Причина должна быть классифицирована:
|
||||
|
||||
- Hardware
|
||||
- Software
|
||||
- Network
|
||||
- Human error
|
||||
- External factor
|
||||
|
||||
3. Запрещено:
|
||||
- закрывать заявку без причины
|
||||
- менять систему без фиксации
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3.4 ОТЧЁТ ТЕХНИКА (ШАБЛОН)
|
||||
|
||||
```text
|
||||
AegisOne Engineering
|
||||
Technician Report
|
||||
|
||||
Объект:
|
||||
Дата:
|
||||
Техник:
|
||||
|
||||
---
|
||||
|
||||
1. Выполненные работы:
|
||||
-
|
||||
|
||||
2. Выявленные проблемы:
|
||||
-
|
||||
|
||||
3. Причина неисправности:
|
||||
(Hardware / Software / Network / Other)
|
||||
|
||||
4. Что было сделано:
|
||||
-
|
||||
|
||||
5. Результат:
|
||||
[ ] Исправлено
|
||||
[ ] Частично исправлено
|
||||
[ ] Требует повторного выезда
|
||||
|
||||
6. Фото до/после:
|
||||
(вложение)
|
||||
|
||||
7. Комментарий:
|
||||
-
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4. ОБЯЗАТЕЛЬНЫЕ ДАННЫЕ ТЕХНИКА В СИСТЕМЕ
|
||||
|
||||
---
|
||||
|
||||
## 4.1 Минимум данных
|
||||
|
||||
- текущие заявки
|
||||
- SLA таймер
|
||||
- адрес объекта
|
||||
- тип системы
|
||||
- приоритет
|
||||
|
||||
---
|
||||
|
||||
## 4.2 Рекомендуемые данные
|
||||
|
||||
- история объекта
|
||||
- прошлые неисправности
|
||||
- схема оборудования
|
||||
- контакт ответственного
|
||||
- тип неисправности по базе
|
||||
|
||||
---
|
||||
|
||||
# 5. КЛЮЧЕВАЯ ЛОГИКА СИСТЕМЫ
|
||||
|
||||
```text
|
||||
Техник не “чинит оборудование”
|
||||
|
||||
Техник:
|
||||
→ закрывает SLA
|
||||
→ влияет на SHS
|
||||
→ формирует retention
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6. КОНТРОЛЬ РИСКОВ
|
||||
|
||||
---
|
||||
|
||||
## РИСК 1 — “закрытие без факта”
|
||||
|
||||
👉 решение:
|
||||
|
||||
- фотофиксация обязательна
|
||||
|
||||
---
|
||||
|
||||
## РИСК 2 — “ложное устранение”
|
||||
|
||||
👉 решение:
|
||||
|
||||
- reopen rate KPI
|
||||
|
||||
---
|
||||
|
||||
## РИСК 3 — перегруз техника
|
||||
|
||||
👉 решение:
|
||||
|
||||
- utilization tracking
|
||||
|
||||
---
|
||||
|
||||
## РИСК 4 — нарушение SLA
|
||||
|
||||
👉 решение:
|
||||
|
||||
- live SLA countdown
|
||||
|
||||
---
|
||||
|
||||
# 7. ИТОГОВАЯ МОДЕЛЬ
|
||||
|
||||
```text
|
||||
CRM → SLA → TASK → TECH DASHBOARD → EXECUTION → KPI → SHS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 8. СУТЬ СИСТЕМЫ
|
||||
|
||||
Ты создаёшь не “работу техника”:
|
||||
|
||||
> ты создаёшь управляемый процесс физической эксплуатации инфраструктуры бизнеса
|
||||
|
||||
---
|
||||
|
||||
# 9. ЕСЛИ ДАЛЬШЕ РАЗВИВАТЬ
|
||||
|
||||
Можно собрать:
|
||||
|
||||
- мобильное приложение техника (PWA)
|
||||
- GPS контроль выездов
|
||||
- автоакты выполненных работ
|
||||
- голосовые отчёты
|
||||
- AI диагностику неисправностей
|
||||
- автоматическое распределение заявок
|
||||
|
||||
---
|
||||
|
||||
Если хочешь следующий шаг:
|
||||
|
||||
👉 я могу объединить дашборд техника + инженера + CEO в одну единую систему управления AegisOne (как ERP уровня SAP, но для твоей ниши)
|
||||
@@ -0,0 +1,574 @@
|
||||
Ниже — система, которая делает тебя не “компанией по безопасности”, а:
|
||||
|
||||
доказательным инженерным экспертом
|
||||
|
||||
Это ключевой переход.
|
||||
Потому что рынок верит не словам, а:
|
||||
|
||||
> измерениям, проверкам и воспроизводимым результатам
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
0. ЧТО ТАКОЕ “ДОКАЗАТЕЛЬНАЯ ЭКСПЕРТИЗА”
|
||||
|
||||
Это подход, где каждое твоё утверждение:
|
||||
|
||||
проверяется тестом
|
||||
|
||||
фиксируется
|
||||
|
||||
документируется
|
||||
|
||||
повторяется на любом объекте
|
||||
|
||||
подтверждается результатом
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. ГЛАВНАЯ ИДЕЯ СИСТЕМЫ
|
||||
|
||||
Ты перестаёшь говорить:
|
||||
|
||||
❌ “у вас плохое обслуживание”
|
||||
❌ “система работает неправильно”
|
||||
|
||||
Ты начинаешь говорить:
|
||||
|
||||
✔ “мы провели тест №X”
|
||||
✔ “результат измерен”
|
||||
✔ “отклонение от нормы”
|
||||
✔ “риск подтверждён”
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. СТРУКТУРА СИСТЕМЫ ДОКАЗАТЕЛЬНОСТИ
|
||||
|
||||
3. Тест (что проверяем)
|
||||
4. Методика (как проверяем)
|
||||
5. Норма (как должно быть)
|
||||
6. Фактический результат
|
||||
7. Отклонение
|
||||
8. Риск для бизнеса
|
||||
9. Вывод инженера
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. БАЗОВЫЕ ТЕСТЫ (ЯДРО ТВОЕЙ ЭКСПЕРТИЗЫ)
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕСТ №1 — РЕАЛЬНОСТЬ АРХИВА ВИДЕОНАБЛЮДЕНИЯ
|
||||
|
||||
|
||||
---
|
||||
|
||||
🔧 Методика
|
||||
|
||||
Выборочно проверяется запись с камер за последние:
|
||||
- 1 день
|
||||
- 7 дней
|
||||
- 14 дней
|
||||
- 30 дней
|
||||
|
||||
Проверяется:
|
||||
- наличие записи
|
||||
- непрерывность
|
||||
- пропуски
|
||||
- доступность воспроизведения
|
||||
|
||||
|
||||
---
|
||||
|
||||
📏 Норма
|
||||
|
||||
запись 100% камер
|
||||
|
||||
непрерывность без пропусков
|
||||
|
||||
доступность архива согласно заявленному сроку
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
📉 Частая реальность
|
||||
|
||||
часть камер не пишет
|
||||
|
||||
архив “дыры”
|
||||
|
||||
перезапись раньше срока
|
||||
|
||||
сбои HDD
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
⚠️ Риск
|
||||
|
||||
> Потеря доказательной базы при инциденте
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
🧠 Заключение от тебя
|
||||
|
||||
> “На объекте система видеонаблюдения формально функционирует, но не гарантирует сохранность событийного архива.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕСТ №2 — СИНХРОНИЗАЦИЯ ВРЕМЕНИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Методика
|
||||
|
||||
Сравнение времени:
|
||||
- на камерах
|
||||
- на регистраторе
|
||||
- на сервере СКУД
|
||||
- фактическое время события
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма
|
||||
|
||||
отклонение ≤ 1–2 секунды
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Часто
|
||||
|
||||
разрыв 2–15 минут
|
||||
|
||||
разные часовые зоны
|
||||
|
||||
нет NTP синхронизации
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риск
|
||||
|
||||
> невозможность юридически доказать момент события
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Вывод
|
||||
|
||||
> “Система не обеспечивает юридически корректную фиксацию времени событий.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕСТ №3 — РЕЗЕРВНОЕ ПИТАНИЕ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Методика
|
||||
|
||||
Проверка:
|
||||
- UPS
|
||||
- время автономной работы
|
||||
- отключение питания
|
||||
- поведение систем
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма
|
||||
|
||||
15–60 минут автономии минимум
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Часто
|
||||
|
||||
UPS “для галочки”
|
||||
|
||||
не держит нагрузку
|
||||
|
||||
отсутствует тестирование
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риск
|
||||
|
||||
> полная остановка системы при отключении электричества
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Вывод
|
||||
|
||||
> “Инфраструктура объекта не защищена от отключения электропитания.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕСТ №4 — ПОЛНОТА КАМЕРНОГО ПОКРЫТИЯ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Методика
|
||||
|
||||
Проверка зон:
|
||||
- входы
|
||||
- кассы
|
||||
- периметр
|
||||
- слепые зоны
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма
|
||||
|
||||
отсутствие “мертвых зон”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Часто
|
||||
|
||||
перекрытия нет
|
||||
|
||||
камеры направлены неправильно
|
||||
|
||||
часть зон не контролируется
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риск
|
||||
|
||||
> невозможность фиксации инцидентов
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Вывод
|
||||
|
||||
> “Фактическое покрытие объекта не соответствует заявленной системе безопасности.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕСТ №5 — ЖУРНАЛ СОБЫТИЙ СКУД
|
||||
|
||||
|
||||
---
|
||||
|
||||
Методика
|
||||
|
||||
Проверка:
|
||||
- записи проходов
|
||||
- корректность пользователей
|
||||
- история событий
|
||||
|
||||
|
||||
---
|
||||
|
||||
Норма
|
||||
|
||||
полный лог всех проходов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Часто
|
||||
|
||||
потери логов
|
||||
|
||||
сбои базы
|
||||
|
||||
отключённый журнал
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риск
|
||||
|
||||
> невозможность отследить перемещения персонала
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Вывод
|
||||
|
||||
> “СКУД не выполняет функцию контроля доступа в полном объёме.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕСТ №6 — СОСТОЯНИЕ ПИТАНИЯ СИСТЕМ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Методика
|
||||
|
||||
Проверка:
|
||||
- напряжения
|
||||
- нагрузки
|
||||
- перегрева
|
||||
- стабильности питания оборудования
|
||||
|
||||
|
||||
---
|
||||
|
||||
Часто
|
||||
|
||||
перегрузка линий
|
||||
|
||||
дешёвые блоки питания
|
||||
|
||||
нестабильное напряжение
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риск
|
||||
|
||||
> деградация оборудования и внезапные отказы
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Вывод
|
||||
|
||||
> “Система имеет скрытую деградацию по питанию.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТЕСТ №7 — СОСТОЯНИЕ ХРАНЕНИЯ ДАННЫХ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Методика
|
||||
|
||||
Проверка:
|
||||
- HDD/SSD
|
||||
- заполнение
|
||||
- циклы перезаписи
|
||||
- ошибки записи
|
||||
|
||||
|
||||
---
|
||||
|
||||
Часто
|
||||
|
||||
диски в деградации
|
||||
|
||||
нет мониторинга
|
||||
|
||||
потеря данных без уведомлений
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риск
|
||||
|
||||
> потеря архива без внешних признаков
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Вывод
|
||||
|
||||
> “Система хранения не контролируется и не мониторится.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. КАК ТЫ ПРЕЗЕНТУЕШЬ ЭТО КЛИЕНТУ
|
||||
|
||||
|
||||
---
|
||||
|
||||
НЕ так:
|
||||
|
||||
❌ “у вас проблемы”
|
||||
|
||||
|
||||
---
|
||||
|
||||
А так:
|
||||
|
||||
✔ “мы провели тест №3 и зафиксировали отклонение”
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. ШАБЛОН ТВОЕГО ЭКСПЕРТНОГО ЗАКЛЮЧЕНИЯ
|
||||
|
||||
ИНЖЕНЕРНОЕ ЗАКЛЮЧЕНИЕ
|
||||
|
||||
Объект: ________
|
||||
|
||||
Проведены тесты:
|
||||
- архив видеонаблюдения
|
||||
- синхронизация времени
|
||||
- резервное питание
|
||||
- СКУД
|
||||
- хранение данных
|
||||
|
||||
Выявленные отклонения:
|
||||
|
||||
1. ________
|
||||
2. ________
|
||||
3. ________
|
||||
|
||||
Риски:
|
||||
|
||||
- ________
|
||||
- ________
|
||||
|
||||
Заключение инженера:
|
||||
|
||||
Система безопасности объекта требует технического сопровождения
|
||||
для обеспечения стабильной эксплуатации и снижения рисков отказов.
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. СИЛА ЭТОЙ СИСТЕМЫ
|
||||
|
||||
Ты становишься:
|
||||
|
||||
не монтажником
|
||||
|
||||
не продавцом услуг
|
||||
|
||||
не подрядчиком
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
А ЧЕМ?
|
||||
|
||||
инженером, который доказывает состояние объекта
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. ПОЧЕМУ ЭТО ПРОДАЁТ SLA
|
||||
|
||||
Потому что клиент видит:
|
||||
|
||||
факты
|
||||
|
||||
отклонения
|
||||
|
||||
риски
|
||||
|
||||
|
||||
И сам приходит к выводу:
|
||||
|
||||
> “это нужно сопровождать постоянно”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. КАК ЭТО МАСШТАБИРУЕТСЯ
|
||||
|
||||
Ты можешь:
|
||||
|
||||
обучить инженеров тестам
|
||||
|
||||
стандартизировать отчёты
|
||||
|
||||
превратить это в продукт аудита
|
||||
|
||||
автоматизировать CRM
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. СЛЕДУЮЩИЙ УРОВЕНЬ (если продолжим)
|
||||
|
||||
Я могу дальше собрать:
|
||||
|
||||
автоматический шаблон отчёта (PDF + PHP генерация)
|
||||
|
||||
систему балльной оценки объекта (risk scoring)
|
||||
|
||||
SLA-переход на основе результатов тестов
|
||||
|
||||
“паспорт объекта безопасности”
|
||||
|
||||
стандарты для инженеров в команде
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Если коротко:
|
||||
|
||||
> ты превращаешь “услуги безопасности” в инженерную измеряемую систему контроля риска
|
||||
|
||||
|
||||
|
||||
И это уже не рынок монтажников.
|
||||
Это рынок эксплуатационных инженеров уровня enterprise.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Инструкция по админ-панели AegisOne
|
||||
|
||||
## Вход в систему
|
||||
|
||||
1. Перейдите на `https://aegisone.ru/service/`
|
||||
2. Введите логин и пароль, выданные владельцем
|
||||
3. При 3 неверных попытках — блокировка на 3 минуты
|
||||
|
||||
## Дашборд
|
||||
|
||||
Главная страница после входа. Карточки показывают ключевые метрики:
|
||||
- **Активные объекты** — количество обслуживаемых объектов
|
||||
- **Активные задачи** — открытые и в работе
|
||||
- **Открытые инциденты** — текущие инциденты
|
||||
- **SLA контракты** — активные договоры
|
||||
- **SLA Compliance** — процент соблюдения SLA
|
||||
- **SHS** — общий индекс здоровья системы
|
||||
|
||||
Нажмите на любую карточку для перехода в соответствующий раздел.
|
||||
|
||||
## Управление сотрудниками (только владелец)
|
||||
|
||||
- Создание учётных записей с ролями: Владелец / Инженер / Техник
|
||||
- Блокировка/разблокировка сотрудников
|
||||
- Сброс пароля
|
||||
|
||||
## Объекты
|
||||
|
||||
- Добавление объектов с параметрами: тип, площадь, регион
|
||||
- Автоматическое создание из опросника
|
||||
- Просмотр Risk Score, Object Index, SLA цены
|
||||
|
||||
## Назначения
|
||||
|
||||
Привязка сотрудников к объектам. Роль сотрудника определяется автоматически из его профиля.
|
||||
|
||||
## SLA контракты
|
||||
|
||||
- Создание контрактов с привязкой к объекту
|
||||
- Номер договора (для синхронизации с 1С)
|
||||
- Уровень обслуживания: Start / Business / Enterprise
|
||||
- Гибкое время реакции (в часах)
|
||||
|
||||
## Опросник объекта
|
||||
|
||||
Пошаговое обследование объекта:
|
||||
1. **Коммерческий** — название, тип, адрес, контакты
|
||||
2. **Технический** — видеонаблюдение, СКУД, пожарная сигнализация, инфраструктура
|
||||
3. **Эксплуатация** — текущее обслуживание, регламенты, проблемы
|
||||
4. **Риски** — критические риски объекта
|
||||
5. **Расчёт SLA** — уровень обслуживания, дополнительные требования
|
||||
|
||||
После завершения автоматически рассчитываются: Risk Score, Complexity Index, Object Index, SLA цена.
|
||||
|
||||
## Блог
|
||||
|
||||
- Создание/редактирование статей
|
||||
- **Slug** — адрес статьи, генерируется автоматически из заголовка (можно изменить вручную)
|
||||
- **Категории**: Аудит, SLA, Инциденты, Технадзор, Документация, Риск-инжиниринг, Кейсы
|
||||
- **Содержание** — HTML-формат. Используйте тулбар над редактором для вставки форматирования
|
||||
- Статусы: Черновик → Опубликовать → Архив
|
||||
|
||||
## Документация
|
||||
|
||||
- Файлы `.md` в папке `service/docs/` автоматически обнаруживаются
|
||||
- Владелец управляет доступом инженеров к каждому документу
|
||||
- Инженер видит только разрешённые документы
|
||||
|
||||
## Примеры из практики (карусель на главной)
|
||||
|
||||
- Добавление кейсов с заголовком, описанием и эффектом
|
||||
- Поле «Порядок» определяет порядок в карусели
|
||||
- Флажок «Активен» — показывает/скрывает кейс на главной
|
||||
|
||||
## CEO дашборд (только владелец)
|
||||
|
||||
Обзор бизнес-метрик: MRR, удержание клиентов, конверсия, производительность.
|
||||
|
||||
## Коэффициенты формул
|
||||
|
||||
Все расчётные коэффициенты можно редактировать. Изменения применяются ко всем новым расчётам.
|
||||
|
||||
## Паспорта объектов
|
||||
|
||||
Автоматически генерируются при завершении опросника. Содержат полный расчёт рисков и рекомендаций.
|
||||
@@ -0,0 +1,589 @@
|
||||
Ниже — уже не просто “текст КП”, а фундамент твоей новой модели бизнеса.
|
||||
|
||||
Я делаю это в формате:
|
||||
|
||||
инженерного B2B;
|
||||
|
||||
без “маркетингового мусора”;
|
||||
|
||||
с акцентом на доверие, SLA и эксплуатацию.
|
||||
|
||||
|
||||
Это можно:
|
||||
|
||||
сразу использовать;
|
||||
|
||||
конвертировать в PDF;
|
||||
|
||||
вставить на сайт;
|
||||
|
||||
генерировать автоматически через CRM.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. ПОЛНОЦЕННЫЙ ШАБЛОН КП (.md)
|
||||
|
||||
# КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ
|
||||
## Техническое сопровождение систем безопасности
|
||||
|
||||
---
|
||||
|
||||
# AEGISONE ENGINEERING
|
||||
### Эксплуатация и сопровождение систем безопасности коммерческих объектов
|
||||
|
||||
---
|
||||
|
||||
## Контакты
|
||||
|
||||
Телефон: +7 (XXX) XXX-XX-XX
|
||||
Email: info@aegisone.ru
|
||||
Сайт: https://aegisone.ru
|
||||
|
||||
---
|
||||
|
||||
# О компании
|
||||
|
||||
AEGISONE ENGINEERING — инженерная сервисная компания,
|
||||
специализирующаяся на техническом сопровождении
|
||||
и эксплуатации систем безопасности коммерческих объектов.
|
||||
|
||||
Мы обеспечиваем:
|
||||
- стабильную работу систем;
|
||||
- контроль инфраструктуры;
|
||||
- регламентное обслуживание;
|
||||
- SLA и контроль сроков реакции;
|
||||
- технический аудит;
|
||||
- сопровождение эксплуатации объектов.
|
||||
|
||||
---
|
||||
|
||||
# Какие проблемы мы решаем
|
||||
|
||||
Большинство неисправностей систем безопасности
|
||||
выявляются только после возникновения проблем:
|
||||
|
||||
- отсутствует архив видеонаблюдения;
|
||||
- системы работают нестабильно;
|
||||
- часть оборудования неисправна;
|
||||
- отсутствует документация;
|
||||
- неисправности копятся месяцами;
|
||||
- подрядчики не несут ответственности;
|
||||
- проверки выявляют критические нарушения.
|
||||
|
||||
Это приводит:
|
||||
- к рискам простоев;
|
||||
- потере контроля;
|
||||
- проблемам при проверках;
|
||||
- финансовым потерям.
|
||||
|
||||
---
|
||||
|
||||
# Решение
|
||||
|
||||
Мы берем на себя техническое сопровождение
|
||||
и контроль работоспособности систем безопасности объекта.
|
||||
|
||||
В рамках сопровождения обеспечиваем:
|
||||
|
||||
- регламентное обслуживание;
|
||||
- диагностику;
|
||||
- контроль состояния оборудования;
|
||||
- аварийное реагирование;
|
||||
- сопровождение эксплуатации;
|
||||
- фотоотчетность;
|
||||
- рекомендации по модернизации;
|
||||
- технический контроль подрядчиков.
|
||||
|
||||
---
|
||||
|
||||
# Состав услуг
|
||||
|
||||
| Услуга | Описание |
|
||||
|---|---|
|
||||
| Регламентные проверки | Контроль состояния систем |
|
||||
| Диагностика | Поиск неисправностей |
|
||||
| Контроль архива | Проверка записи и хранения |
|
||||
| Проверка питания | Бесперебойность работы |
|
||||
| Аварийные выезды | Реагирование по SLA |
|
||||
| Фотоотчетность | Подтверждение работ |
|
||||
| Ведение журналов | История обслуживания |
|
||||
| Консультации | Поддержка эксплуатации |
|
||||
|
||||
---
|
||||
|
||||
# SLA
|
||||
|
||||
| Приоритет | Описание | Время реакции |
|
||||
|---|---|---|
|
||||
| P1 | Полный отказ критической системы | до 2 часов |
|
||||
| P2 | Частичная потеря функционала | до 4 часов |
|
||||
| P3 | Некритичная неисправность | до 24 часов |
|
||||
| P4 | Плановые работы | по графику |
|
||||
|
||||
---
|
||||
|
||||
# Почему выбирают нас
|
||||
|
||||
- более 20 лет инженерного опыта;
|
||||
- лицензия МЧС;
|
||||
- работа по SLA и регламентам;
|
||||
- опыт эксплуатации коммерческих объектов;
|
||||
- прозрачная отчетность;
|
||||
- несем ответственность за результат;
|
||||
- не работаем по принципу «сделали и забыли».
|
||||
|
||||
---
|
||||
|
||||
# Тарифы
|
||||
|
||||
## START
|
||||
Для небольших объектов.
|
||||
|
||||
Включает:
|
||||
- ежемесячный регламент;
|
||||
- диагностику;
|
||||
- удаленную поддержку;
|
||||
- отчетность.
|
||||
|
||||
Стоимость:
|
||||
от 25 000 ₽ / месяц
|
||||
|
||||
---
|
||||
|
||||
## BUSINESS
|
||||
Для коммерческих объектов.
|
||||
|
||||
Включает:
|
||||
- SLA;
|
||||
- аварийные выезды;
|
||||
- контроль архива;
|
||||
- сопровождение проверок;
|
||||
- фотоотчетность.
|
||||
|
||||
Стоимость:
|
||||
от 60 000 ₽ / месяц
|
||||
|
||||
---
|
||||
|
||||
## ENTERPRISE
|
||||
Формат внешнего инженерного отдела.
|
||||
|
||||
Включает:
|
||||
- постоянное сопровождение;
|
||||
- участие в эксплуатации;
|
||||
- контроль подрядчиков;
|
||||
- развитие инфраструктуры;
|
||||
- аудит объектов.
|
||||
|
||||
Стоимость:
|
||||
индивидуально
|
||||
|
||||
---
|
||||
|
||||
# Этапы работы
|
||||
|
||||
1. Предварительная консультация
|
||||
2. Технический аудит объекта
|
||||
3. Подготовка отчета и рекомендаций
|
||||
4. Формирование SLA и регламентов
|
||||
5. Постановка объекта на сопровождение
|
||||
6. Регулярное обслуживание и отчетность
|
||||
|
||||
---
|
||||
|
||||
# Технический аудит
|
||||
|
||||
Перед постановкой объекта на сопровождение
|
||||
рекомендуем проведение технического аудита.
|
||||
|
||||
Аудит позволяет:
|
||||
- выявить скрытые проблемы;
|
||||
- оценить состояние систем;
|
||||
- определить риски;
|
||||
- подготовить рекомендации.
|
||||
|
||||
---
|
||||
|
||||
# Контакты
|
||||
|
||||
AEGISONE ENGINEERING
|
||||
|
||||
Телефон:
|
||||
+7 (XXX) XXX-XX-XX
|
||||
|
||||
Email:
|
||||
info@aegisone.ru
|
||||
|
||||
Сайт:
|
||||
https://aegisone.ru
|
||||
---
|
||||
|
||||
2. СТРУКТУРА ПЕРВОГО АУДИТА
|
||||
|
||||
Вот здесь начинается твоя реальная экспертная модель.
|
||||
|
||||
ЦЕЛЬ АУДИТА
|
||||
|
||||
НЕ:
|
||||
|
||||
“найти поломку”.
|
||||
|
||||
|
||||
А:
|
||||
|
||||
показать уровень инженерной зрелости объекта.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЧТО ДОЛЖЕН ДАВАТЬ АУДИТ
|
||||
|
||||
После него клиент должен понять:
|
||||
|
||||
где риски;
|
||||
|
||||
что не работает;
|
||||
|
||||
что может привести к проблемам;
|
||||
|
||||
насколько объект управляем;
|
||||
|
||||
почему нужен SLA.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ИДЕАЛЬНАЯ СТРУКТУРА АУДИТА
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЭТАП 1
|
||||
|
||||
ВВОДНАЯ ИНФОРМАЦИЯ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Собираем:
|
||||
|
||||
тип объекта;
|
||||
|
||||
площадь;
|
||||
|
||||
количество систем;
|
||||
|
||||
ответственные лица;
|
||||
|
||||
история проблем;
|
||||
|
||||
подрядчики;
|
||||
|
||||
наличие документации.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЭТАП 2
|
||||
|
||||
ВИЗУАЛЬНЫЙ ОСМОТР
|
||||
|
||||
|
||||
---
|
||||
|
||||
Проверяем:
|
||||
|
||||
шкафы;
|
||||
|
||||
коммутацию;
|
||||
|
||||
маркировку;
|
||||
|
||||
кабельные трассы;
|
||||
|
||||
питание;
|
||||
|
||||
серверные;
|
||||
|
||||
доступ к оборудованию.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЭТАП 3
|
||||
|
||||
ПРОВЕРКА РАБОТОСПОСОБНОСТИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Видеонаблюдение:
|
||||
|
||||
запись;
|
||||
|
||||
архив;
|
||||
|
||||
синхронизация времени;
|
||||
|
||||
качество изображения;
|
||||
|
||||
доступность камер;
|
||||
|
||||
питание.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
СКУД:
|
||||
|
||||
проходы;
|
||||
|
||||
журналы;
|
||||
|
||||
права доступа;
|
||||
|
||||
аварийное открытие;
|
||||
|
||||
контроллеры.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пожарка:
|
||||
|
||||
индикация;
|
||||
|
||||
ошибки;
|
||||
|
||||
оповещение;
|
||||
|
||||
связь;
|
||||
|
||||
резервное питание.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЭТАП 4
|
||||
|
||||
ПРОВЕРКА ДОКУМЕНТАЦИИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Проверяем:
|
||||
|
||||
схемы;
|
||||
|
||||
пароли;
|
||||
|
||||
IP;
|
||||
|
||||
журналы;
|
||||
|
||||
проекты;
|
||||
|
||||
исполнительную документацию.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЭТАП 5
|
||||
|
||||
ОЦЕНКА РИСКОВ
|
||||
|
||||
Это важнейшая часть.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Например:
|
||||
|
||||
Риск Последствие
|
||||
|
||||
Не пишется архив Потеря доказательств
|
||||
Нет резервного питания Полный отказ
|
||||
Нет документации Долгое восстановление
|
||||
Ошибки пожарки Риски проверок
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЭТАП 6
|
||||
|
||||
РЕКОМЕНДАЦИИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Разделяем:
|
||||
|
||||
Критические
|
||||
|
||||
Исправить срочно.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Важные
|
||||
|
||||
В течение 30 дней.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Плановые
|
||||
|
||||
При модернизации.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЭТАП 7
|
||||
|
||||
ПРЕДЛОЖЕНИЕ SLA
|
||||
|
||||
Вот тут ты переводишь аудит:
|
||||
|
||||
в абонентское сопровождение.
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. ЧЕК-ЛИСТ АУДИТА
|
||||
|
||||
Это уже реальный инструмент продаж.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# ЧЕК-ЛИСТ ТЕХНИЧЕСКОГО АУДИТА
|
||||
|
||||
## Общая информация
|
||||
|
||||
- [ ] Тип объекта
|
||||
- [ ] Адрес
|
||||
- [ ] Ответственное лицо
|
||||
- [ ] Количество систем
|
||||
- [ ] Наличие документации
|
||||
- [ ] Наличие подрядчиков
|
||||
|
||||
---
|
||||
|
||||
# Видеонаблюдение
|
||||
|
||||
## Сервер / регистратор
|
||||
|
||||
- [ ] Проверка записи
|
||||
- [ ] Проверка архива
|
||||
- [ ] Проверка HDD
|
||||
- [ ] Проверка времени
|
||||
- [ ] Проверка сети
|
||||
- [ ] Проверка питания
|
||||
|
||||
## Камеры
|
||||
|
||||
- [ ] Наличие сигнала
|
||||
- [ ] Качество изображения
|
||||
- [ ] Наличие загрязнений
|
||||
- [ ] Корректность обзора
|
||||
- [ ] Наличие ИК подсветки
|
||||
|
||||
---
|
||||
|
||||
# СКУД
|
||||
|
||||
- [ ] Проверка контроллеров
|
||||
- [ ] Проверка считывателей
|
||||
- [ ] Проверка журналов
|
||||
- [ ] Проверка доступа
|
||||
- [ ] Проверка аварийного открытия
|
||||
- [ ] Проверка резервного питания
|
||||
|
||||
---
|
||||
|
||||
# Пожарная сигнализация
|
||||
|
||||
- [ ] Проверка панели
|
||||
- [ ] Проверка ошибок
|
||||
- [ ] Проверка шлейфов
|
||||
- [ ] Проверка оповещения
|
||||
- [ ] Проверка резервного питания
|
||||
- [ ] Проверка журналов событий
|
||||
|
||||
---
|
||||
|
||||
# Сетевая инфраструктура
|
||||
|
||||
- [ ] Проверка коммутаторов
|
||||
- [ ] Проверка UPS
|
||||
- [ ] Проверка кабельной инфраструктуры
|
||||
- [ ] Проверка интернет-каналов
|
||||
- [ ] Проверка температурного режима
|
||||
|
||||
---
|
||||
|
||||
# Документация
|
||||
|
||||
- [ ] Схемы
|
||||
- [ ] IP-адресация
|
||||
- [ ] Пароли
|
||||
- [ ] Журналы обслуживания
|
||||
- [ ] Исполнительная документация
|
||||
|
||||
---
|
||||
|
||||
# Риски
|
||||
|
||||
- [ ] Отсутствие записи
|
||||
- [ ] Отказ резервного питания
|
||||
- [ ] Отсутствие документации
|
||||
- [ ] Ошибки оборудования
|
||||
- [ ] Нестабильность сети
|
||||
|
||||
---
|
||||
|
||||
# Рекомендации
|
||||
|
||||
## Критические:
|
||||
_________________________________
|
||||
|
||||
## Важные:
|
||||
_________________________________
|
||||
|
||||
## Плановые:
|
||||
_________________________________
|
||||
---
|
||||
|
||||
САМОЕ ВАЖНОЕ
|
||||
|
||||
Вот что тебе нужно понять стратегически.
|
||||
|
||||
АУДИТ — ЭТО НЕ “ДОП. УСЛУГА”.
|
||||
|
||||
Это:
|
||||
|
||||
твоя система продаж.
|
||||
|
||||
Потому что:
|
||||
|
||||
не надо “впаривать”;
|
||||
|
||||
ты показываешь реальность;
|
||||
|
||||
ты становишься экспертом;
|
||||
|
||||
клиент сам понимает необходимость SLA.
|
||||
|
||||
|
||||
И это идеально подходит под твою модель инженерного B2B.
|
||||
@@ -0,0 +1,503 @@
|
||||
Хорошо. Здесь ты фактически строишь не “контент”, а:
|
||||
|
||||
инженерную медийную репутацию компании
|
||||
|
||||
Это важно различать.
|
||||
|
||||
Контент в твоей нише выполняет 3 функции:
|
||||
|
||||
1. формирует доверие у B2B (директора, УК, собственники)
|
||||
|
||||
|
||||
2. объясняет, почему эксплуатация важнее монтажа
|
||||
|
||||
|
||||
3. подводит к SLA и аудитам (деньгам)
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
0. СТРАТЕГИЯ ЭКСПЕРТНОСТИ (ОСНОВА)
|
||||
|
||||
Твоя роль в контенте:
|
||||
|
||||
> “Спокойный инженер, который объясняет, где у бизнеса скрытые риски в системах безопасности”
|
||||
|
||||
|
||||
|
||||
НЕ:
|
||||
|
||||
продавец
|
||||
|
||||
маркетолог
|
||||
|
||||
“лиды”
|
||||
|
||||
“акции”
|
||||
|
||||
|
||||
А:
|
||||
|
||||
диагност
|
||||
|
||||
эксплуатационный инженер
|
||||
|
||||
человек, который видит риски до аварии
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. КОНТЕНТ-СТОЛПЫ (PILLARS)
|
||||
|
||||
У тебя должно быть 5 контент-направлений:
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. Эксплуатационные проблемы (самый важный)
|
||||
|
||||
👉 “Что ломается в реальности”
|
||||
|
||||
Примеры:
|
||||
|
||||
камеры не пишут архив
|
||||
|
||||
пожарка работает “на бумаге”
|
||||
|
||||
СКУД пропускает ошибки
|
||||
|
||||
нет резервного питания
|
||||
|
||||
подрядчики исчезают
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. Разбор реальных кейсов (без раскрытия клиента)
|
||||
|
||||
👉 “Что мы нашли на объектах”
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. Ошибки монтажа и эксплуатации
|
||||
|
||||
👉 “Почему дешево = дорого”
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. Инженерные разборы систем
|
||||
|
||||
👉 “как это должно работать правильно”
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. Управление рисками (B2B язык)
|
||||
|
||||
👉 “что теряет бизнес”
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. ЧАСТОТА ПУБЛИКАЦИЙ
|
||||
|
||||
Идеально:
|
||||
|
||||
2 поста в неделю (Telegram / VK / VC)
|
||||
|
||||
1 длинная статья в неделю
|
||||
|
||||
1 кейс в неделю (может совпадать)
|
||||
|
||||
|
||||
Итого: 👉 8–10 единиц контента / месяц
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. КОНТЕНТ-ПЛАН НА 3 МЕСЯЦА
|
||||
|
||||
|
||||
---
|
||||
|
||||
МЕСЯЦ 1 — “ПРОБЛЕМЫ РЫНКА”
|
||||
|
||||
Цель: 👉 показать, что рынок систем безопасности работает плохо
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 1
|
||||
|
||||
Пост 1
|
||||
|
||||
Почему камеры видеонаблюдения не защищают бизнес
|
||||
|
||||
Пост 2
|
||||
|
||||
Что происходит, когда не ведется обслуживание систем безопасности
|
||||
|
||||
Статья:
|
||||
|
||||
ТОП-7 скрытых проблем систем безопасности на коммерческих объектах
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 2
|
||||
|
||||
Пост:
|
||||
|
||||
Почему 80% объектов не имеют реального архива видеонаблюдения
|
||||
|
||||
Пост:
|
||||
|
||||
Как подрядчики “сдают” объект и исчезают
|
||||
|
||||
Кейс:
|
||||
|
||||
“Объект без архива 4 месяца — как это обнаруживается”
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 3
|
||||
|
||||
Пост:
|
||||
|
||||
Почему пожарная сигнализация чаще всего существует “на бумаге”
|
||||
|
||||
Пост:
|
||||
|
||||
3 ошибки, которые делают монтажники при установке СКУД
|
||||
|
||||
Статья:
|
||||
|
||||
Почему системы безопасности не работают в момент инцидента
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 4
|
||||
|
||||
Пост:
|
||||
|
||||
Что на самом деле означает “обслуживание систем безопасности”
|
||||
|
||||
Пост:
|
||||
|
||||
Почему дешёвый монтаж всегда превращается в дорогое обслуживание
|
||||
|
||||
Кейс:
|
||||
|
||||
“Склад с критическими ошибками в системе доступа”
|
||||
|
||||
|
||||
---
|
||||
|
||||
МЕСЯЦ 2 — “ЭКСПЕРТНОСТЬ”
|
||||
|
||||
Цель: 👉 показать, что ты знаешь, как должно быть правильно
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 1
|
||||
|
||||
Пост:
|
||||
|
||||
Как правильно проверять видеонаблюдение на объекте
|
||||
|
||||
Статья:
|
||||
|
||||
Чек-лист эксплуатации систем безопасности для бизнеса
|
||||
|
||||
Кейс:
|
||||
|
||||
“Восстановление системы видеонаблюдения после 2 лет хаоса”
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 2
|
||||
|
||||
Пост:
|
||||
|
||||
Что должен контролировать собственник объекта
|
||||
|
||||
Пост:
|
||||
|
||||
Почему отсутствие SLA = потеря контроля
|
||||
|
||||
Статья:
|
||||
|
||||
Как устроена правильная эксплуатация систем безопасности
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 3
|
||||
|
||||
Пост:
|
||||
|
||||
Как проверить, работает ли ваша система безопасности реально
|
||||
|
||||
Пост:
|
||||
|
||||
Почему регламент важнее оборудования
|
||||
|
||||
Кейс:
|
||||
|
||||
“Объект после смены подрядчика”
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 4
|
||||
|
||||
Пост:
|
||||
|
||||
Что такое инженерная эксплуатация (а не монтаж)
|
||||
|
||||
Статья:
|
||||
|
||||
Почему рынок безопасности живет в иллюзии надежности
|
||||
|
||||
|
||||
---
|
||||
|
||||
МЕСЯЦ 3 — “ДОВЕРИЕ И ПЕРЕХОД В ПРОДАЖИ”
|
||||
|
||||
Цель: 👉 мягко переводить в аудит и SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 1
|
||||
|
||||
Пост:
|
||||
|
||||
Что показывает технический аудит объекта
|
||||
|
||||
Статья:
|
||||
|
||||
Как мы выявляем скрытые проблемы систем безопасности
|
||||
|
||||
Кейс:
|
||||
|
||||
“Аудит коммерческого объекта: 12 критических ошибок”
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 2
|
||||
|
||||
Пост:
|
||||
|
||||
Почему бизнесу нужен внешний инженер безопасности
|
||||
|
||||
Пост:
|
||||
|
||||
Что происходит, когда нет технического контроля
|
||||
|
||||
Статья:
|
||||
|
||||
SLA в системах безопасности: зачем он нужен
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 3
|
||||
|
||||
Пост:
|
||||
|
||||
Как выглядит нормальное техническое сопровождение
|
||||
|
||||
Кейс:
|
||||
|
||||
“Перевод объекта на SLA обслуживание”
|
||||
|
||||
Пост:
|
||||
|
||||
Почему обслуживание = защита бизнеса, а не ремонт
|
||||
|
||||
|
||||
---
|
||||
|
||||
Неделя 4
|
||||
|
||||
Пост:
|
||||
|
||||
Когда нужно делать аудит (и почему его откладывают)
|
||||
|
||||
Статья:
|
||||
|
||||
Как мы снижаем эксплуатационные риски объектов
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ФОРМАТ КОНТЕНТА (ВАЖНО)
|
||||
|
||||
Каждый пост должен иметь структуру:
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. Проблема
|
||||
|
||||
2. Реальный инженерный разбор
|
||||
|
||||
3. Последствия для бизнеса
|
||||
|
||||
4. Вывод (очень спокойный)
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. ПРИМЕР ЭКСПЕРТНОГО ПОСТА (ГОТОВЫЙ)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Тема:
|
||||
|
||||
“Почему камеры видеонаблюдения не защищают бизнес”
|
||||
|
||||
|
||||
---
|
||||
|
||||
Видеонаблюдение часто воспринимается как система безопасности.
|
||||
|
||||
На практике это не всегда так.
|
||||
|
||||
Основная проблема заключается не в оборудовании, а в эксплуатации.
|
||||
|
||||
На большинстве объектов мы наблюдаем типовые ситуации:
|
||||
|
||||
— часть камер не записывает архив;
|
||||
— запись есть, но не сохраняется нужный период;
|
||||
— часть каналов отключена;
|
||||
— время на устройствах не синхронизировано;
|
||||
— отсутствует резервирование питания;
|
||||
— система не тестируется регулярно.
|
||||
|
||||
В результате при инциденте система формально есть, но фактически не выполняет свою функцию.
|
||||
|
||||
Бизнес узнаёт об этом только постфактум.
|
||||
|
||||
Правильная эксплуатация видеонаблюдения — это не установка оборудования, а регулярный контроль:
|
||||
|
||||
— состояния записи;
|
||||
— целостности архива;
|
||||
— доступности камер;
|
||||
— корректности времени;
|
||||
— работоспособности хранения.
|
||||
|
||||
Без этого система превращается в формальность.
|
||||
|
||||
Именно поэтому в инженерной эксплуатации видеонаблюдение рассматривается как сервис, а не как установленное оборудование.
|
||||
---
|
||||
|
||||
6. КОД ДЛЯ КОНТЕНТ-СТРАНИЦЫ (PHP БЛОГ)
|
||||
|
||||
|
||||
---
|
||||
|
||||
<?php include '../header.php'; ?>
|
||||
|
||||
<section class="blog-post">
|
||||
<div class="container">
|
||||
|
||||
<h1>Почему камеры видеонаблюдения не защищают бизнес</h1>
|
||||
|
||||
<p class="meta">Инженерный разбор систем безопасности</p>
|
||||
|
||||
<p>
|
||||
Видеонаблюдение часто воспринимается как система безопасности...
|
||||
</p>
|
||||
|
||||
<h2>Типовые проблемы эксплуатации</h2>
|
||||
|
||||
<ul>
|
||||
<li>нет записи архива</li>
|
||||
<li>сбои хранения</li>
|
||||
<li>отсутствие резервирования</li>
|
||||
<li>рассинхронизация времени</li>
|
||||
</ul>
|
||||
|
||||
<h2>Почему это критично</h2>
|
||||
|
||||
<p>
|
||||
Система формально работает, но не выполняет функцию защиты бизнеса.
|
||||
</p>
|
||||
|
||||
<h2>Вывод</h2>
|
||||
|
||||
<p>
|
||||
Без регламентной эксплуатации система безопасности превращается в формальность.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include '../footer.php'; ?>
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. ГЛАВНЫЙ ПРИНЦИП ВСЕГО КОНТЕНТА
|
||||
|
||||
Ты должен повторять одну мысль:
|
||||
|
||||
> “Без эксплуатации система безопасности не существует”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. ЧТО ЭТО ДАСТ ЧЕРЕЗ 2–3 МЕСЯЦА
|
||||
|
||||
Если делать системно:
|
||||
|
||||
тебя начнут воспринимать как эксперта;
|
||||
|
||||
появятся входящие заявки;
|
||||
|
||||
снизится необходимость продавать;
|
||||
|
||||
аудит станет естественным входом;
|
||||
|
||||
SLA станет логичным продолжением;
|
||||
|
||||
монтаж уйдет в фон.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. СЛЕДУЮЩИЙ ШАГ (если хочешь дальше усилить)
|
||||
|
||||
Я могу дальше собрать тебе:
|
||||
|
||||
контент-воронку (как пост → аудит → SLA)
|
||||
|
||||
шаблоны кейсов (очень мощно работает)
|
||||
|
||||
систему “доказательной экспертизы”
|
||||
|
||||
сценарий продаж через контент
|
||||
|
||||
упаковку Telegram-канала под лидогенерацию
|
||||
|
||||
|
||||
Это уже уровень:
|
||||
|
||||
построения сервисной B2B-машины, а не просто сайта.
|
||||
@@ -0,0 +1,673 @@
|
||||
Вот здесь начинается настоящий B2B.
|
||||
И это как раз твоя сильная сторона.
|
||||
|
||||
Потому что:
|
||||
|
||||
# тебе НЕ нужен агрессивный маркетинг.
|
||||
|
||||
Ты не:
|
||||
|
||||
- франшиза,
|
||||
- массовый монтажник,
|
||||
- “льем лиды”.
|
||||
|
||||
Твой рынок:
|
||||
|
||||
# доверительный инженерный B2B.
|
||||
|
||||
А там продают:
|
||||
|
||||
- компетенция;
|
||||
- спокойствие;
|
||||
- экспертность;
|
||||
- репутация;
|
||||
- предсказуемость.
|
||||
|
||||
---
|
||||
|
||||
# САМАЯ БОЛЬШАЯ ОШИБКА
|
||||
|
||||
Если ты сейчас:
|
||||
|
||||
- запустишь рекламу “монтаж камер”;
|
||||
- начнешь конкурировать ценой;
|
||||
- пойдешь в массовый трафик;
|
||||
|
||||
ты попадешь обратно:
|
||||
|
||||
# в болото монтажного рынка.
|
||||
|
||||
---
|
||||
|
||||
# ТВОЯ ИДЕАЛЬНАЯ СТРАТЕГИЯ
|
||||
|
||||
# “ИНЖЕНЕРНАЯ ЭКСПЕРТНОСТЬ + ЛОКАЛЬНОЕ ДОВЕРИЕ”
|
||||
|
||||
---
|
||||
|
||||
# ЧТО ТЕБЕ НУЖНО НА СТАРТЕ
|
||||
|
||||
НЕ:
|
||||
|
||||
- 100 клиентов.
|
||||
|
||||
А:
|
||||
|
||||
# 3–5 правильных объектов.
|
||||
|
||||
Это принципиально.
|
||||
|
||||
---
|
||||
|
||||
# ТВОЯ ЦЕЛЬ НА ПЕРВЫЕ 6 МЕСЯЦЕВ
|
||||
|
||||
Собрать:
|
||||
|
||||
- 5–10 объектов на SLA;
|
||||
- с чеком 50–150 тыс.
|
||||
|
||||
Это уже:
|
||||
|
||||
# 500 тыс – 1.5 млн recurring revenue.
|
||||
|
||||
И это достижимо без рекламы.
|
||||
|
||||
---
|
||||
|
||||
# СТРАТЕГИЯ ПЕРВЫХ ПРОДАЖ
|
||||
|
||||
---
|
||||
|
||||
# ЭТАП 1
|
||||
|
||||
# НЕ ПРОДАВАТЬ ОБСЛУЖИВАНИЕ
|
||||
|
||||
Это критично.
|
||||
|
||||
Потому что: “ТО” воспринимается как:
|
||||
|
||||
- обязаловка;
|
||||
- минималка;
|
||||
- формальность.
|
||||
|
||||
---
|
||||
|
||||
# ПРОДАВАТЬ НУЖНО:
|
||||
|
||||
# АУДИТ И СНИЖЕНИЕ РИСКОВ.
|
||||
|
||||
---
|
||||
|
||||
# ТВОЙ ИДЕАЛЬНЫЙ ВХОД
|
||||
|
||||
НЕ:
|
||||
|
||||
> “давайте мы вас обслужим”.
|
||||
|
||||
А:
|
||||
|
||||
# “давайте проверим текущее состояние систем”.
|
||||
|
||||
---
|
||||
|
||||
# ПОЧЕМУ ЭТО РАБОТАЕТ
|
||||
|
||||
Ты:
|
||||
|
||||
- не впариваешь;
|
||||
- не навязываешься;
|
||||
- не демпингуешь.
|
||||
|
||||
Ты:
|
||||
|
||||
# инженер-эксперт.
|
||||
|
||||
---
|
||||
|
||||
# ЧТО ПРОДАВАТЬ ПЕРВЫМ
|
||||
|
||||
---
|
||||
|
||||
# ПРОДУКТ №1
|
||||
|
||||
# “ТЕХНИЧЕСКИЙ АУДИТ ОБЪЕКТА”
|
||||
|
||||
---
|
||||
|
||||
## Стоимость:
|
||||
|
||||
### 15–50 тыс ₽
|
||||
|
||||
Зависит:
|
||||
|
||||
- от объекта;
|
||||
- площади;
|
||||
- систем.
|
||||
|
||||
---
|
||||
|
||||
# ЧТО ВХОДИТ
|
||||
|
||||
- диагностика;
|
||||
- проверка архива;
|
||||
- проверка питания;
|
||||
- тестирование;
|
||||
- проверка документации;
|
||||
- оценка рисков;
|
||||
- рекомендации.
|
||||
|
||||
---
|
||||
|
||||
# РЕЗУЛЬТАТ:
|
||||
|
||||
PDF-отчет.
|
||||
|
||||
---
|
||||
|
||||
# ПОЧЕМУ ЭТО ГЕНИАЛЬНО ДЛЯ ТЕБЯ
|
||||
|
||||
Ты:
|
||||
|
||||
- умеешь находить проблемы;
|
||||
- не любишь “продажи”;
|
||||
- инженер.
|
||||
|
||||
То есть:
|
||||
|
||||
# это идеальная модель продаж под тебя.
|
||||
|
||||
---
|
||||
|
||||
# ЭТАП 2
|
||||
|
||||
# ПРОДАЖА SLA
|
||||
|
||||
После аудита.
|
||||
|
||||
---
|
||||
|
||||
# СХЕМА
|
||||
|
||||
## Ты показываешь:
|
||||
|
||||
### Сейчас:
|
||||
|
||||
- риски;
|
||||
- неисправности;
|
||||
- слабые места.
|
||||
|
||||
---
|
||||
|
||||
## Потом:
|
||||
|
||||
> “Чтобы это не накапливалось — нужен регламент эксплуатации.”
|
||||
|
||||
---
|
||||
|
||||
# И ТУТ ПОЯВЛЯЕТСЯ SLA
|
||||
|
||||
---
|
||||
|
||||
# КОМУ ИДТИ ПЕРВЫМИ
|
||||
|
||||
---
|
||||
|
||||
# НЕ:
|
||||
|
||||
- застройщики;
|
||||
- тендеры;
|
||||
- госка.
|
||||
|
||||
Это болото.
|
||||
|
||||
---
|
||||
|
||||
# ИДЕАЛЬНЫЕ ПЕРВЫЕ КЛИЕНТЫ
|
||||
|
||||
---
|
||||
|
||||
# 1. ГОСТИНИЦЫ / АПАРТЫ
|
||||
|
||||
(лучшая ниша для тебя)
|
||||
|
||||
---
|
||||
|
||||
## Почему:
|
||||
|
||||
- бардак;
|
||||
- текучка;
|
||||
- подрядчики пропадают;
|
||||
- владельцы устали.
|
||||
|
||||
---
|
||||
|
||||
# 2. НЕБОЛЬШИЕ СКЛАДЫ
|
||||
|
||||
Очень хорошая ниша.
|
||||
|
||||
---
|
||||
|
||||
# 3. КОММЕРЧЕСКИЕ УК
|
||||
|
||||
Золото.
|
||||
|
||||
---
|
||||
|
||||
# 4. ЧАСТНЫЕ КЛИНИКИ
|
||||
|
||||
Отличная ниша.
|
||||
|
||||
---
|
||||
|
||||
# 5. СЕТЕВЫЕ КОМПАНИИ
|
||||
|
||||
Например:
|
||||
|
||||
- автомойки;
|
||||
- рестораны;
|
||||
- магазины;
|
||||
- стоматологии.
|
||||
|
||||
---
|
||||
|
||||
# КАК ИСКАТЬ КЛИЕНТОВ БЕЗ РЕКЛАМЫ
|
||||
|
||||
Вот тут твой рынок.
|
||||
|
||||
---
|
||||
|
||||
# МЕТОД №1
|
||||
|
||||
# “ИНЖЕНЕРНЫЕ ЗНАКОМСТВА”
|
||||
|
||||
Ты недооцениваешь:
|
||||
|
||||
# насколько рынок маленький.
|
||||
|
||||
---
|
||||
|
||||
# ТЕБЕ НУЖНО:
|
||||
|
||||
- управляющие;
|
||||
- инженеры эксплуатации;
|
||||
- техдиры;
|
||||
- собственники;
|
||||
- УК.
|
||||
|
||||
---
|
||||
|
||||
# НЕ ПРОДАВАТЬ
|
||||
|
||||
А:
|
||||
|
||||
# знакомиться.
|
||||
|
||||
---
|
||||
|
||||
# ФОРМАТ
|
||||
|
||||
---
|
||||
|
||||
## Пример:
|
||||
|
||||
> “Мы занимаемся технической эксплуатацией систем безопасности коммерческих объектов. Если будет нужна диагностика или помощь — обращайтесь.”
|
||||
|
||||
---
|
||||
|
||||
# ВСЕ.
|
||||
|
||||
Никаких:
|
||||
|
||||
- впариваний;
|
||||
- “акций”;
|
||||
- скидок.
|
||||
|
||||
---
|
||||
|
||||
# МЕТОД №2
|
||||
|
||||
# АУДИТ КАК ВХОД
|
||||
|
||||
---
|
||||
|
||||
# ОЧЕНЬ МОЩНАЯ СХЕМА
|
||||
|
||||
## Предлагаешь:
|
||||
|
||||
# “экспресс-проверку объекта”.
|
||||
|
||||
---
|
||||
|
||||
## Например:
|
||||
|
||||
- проверка архива;
|
||||
- отказоустойчивость;
|
||||
- питание;
|
||||
- документация.
|
||||
|
||||
---
|
||||
|
||||
# ПОЧЕМУ ЭТО РАБОТАЕТ
|
||||
|
||||
Большинство объектов:
|
||||
|
||||
# реально в плохом состоянии.
|
||||
|
||||
---
|
||||
|
||||
# МЕТОД №3
|
||||
|
||||
# LINKEDIN / TELEGRAM / VC / ДЗЕН
|
||||
|
||||
Это твоя золотая жила.
|
||||
|
||||
---
|
||||
|
||||
# ПОЧЕМУ
|
||||
|
||||
Рынок безопасности:
|
||||
|
||||
# чудовищно слаб по экспертности.
|
||||
|
||||
---
|
||||
|
||||
# А ТЫ:
|
||||
|
||||
- умеешь говорить по делу;
|
||||
- инженер;
|
||||
- системный.
|
||||
|
||||
---
|
||||
|
||||
# ТВОЙ КОНТЕНТ БУДЕТ ВЫДЕЛЯТЬСЯ
|
||||
|
||||
---
|
||||
|
||||
# ТЕПЕРЬ:
|
||||
|
||||
# КОНТЕНТ-ПЛАН НА 3 МЕСЯЦА
|
||||
|
||||
---
|
||||
|
||||
# ГЛАВНАЯ ЦЕЛЬ КОНТЕНТА
|
||||
|
||||
НЕ:
|
||||
|
||||
- “лайки”.
|
||||
|
||||
А:
|
||||
|
||||
# доверие B2B.
|
||||
|
||||
---
|
||||
|
||||
# ТВОЯ РОЛЬ В КОНТЕНТЕ
|
||||
|
||||
НЕ: “маркетолог”.
|
||||
|
||||
А:
|
||||
|
||||
# “спокойный инженер-эксперт”.
|
||||
|
||||
---
|
||||
|
||||
# СТИЛЬ КОНТЕНТА
|
||||
|
||||
---
|
||||
|
||||
# НЕ:
|
||||
|
||||
- хайп;
|
||||
- кликбейт;
|
||||
- “ТОП-5 камер”.
|
||||
|
||||
---
|
||||
|
||||
# А:
|
||||
|
||||
- реальные проблемы;
|
||||
- эксплуатация;
|
||||
- ошибки;
|
||||
- риски;
|
||||
- практика.
|
||||
|
||||
---
|
||||
|
||||
# КАНАЛЫ
|
||||
|
||||
---
|
||||
|
||||
# ОБЯЗАТЕЛЬНО:
|
||||
|
||||
- Telegram;
|
||||
- сайт/блог;
|
||||
- Яндекс Бизнес;
|
||||
- VC.ru;
|
||||
- Дзен.
|
||||
|
||||
---
|
||||
|
||||
# МОЖНО:
|
||||
|
||||
- YouTube Shorts;
|
||||
- Rutube.
|
||||
|
||||
---
|
||||
|
||||
# ГЛАВНОЕ:
|
||||
|
||||
# не количество.
|
||||
|
||||
А:
|
||||
|
||||
# экспертность.
|
||||
|
||||
---
|
||||
|
||||
# КОНТЕНТ-ПЛАН
|
||||
|
||||
---
|
||||
|
||||
# МЕСЯЦ 1
|
||||
|
||||
# “ПОКАЗАТЬ ПРОБЛЕМЫ РЫНКА”
|
||||
|
||||
---
|
||||
|
||||
## Неделя 1
|
||||
|
||||
### Статья:
|
||||
|
||||
# Почему камеры не помогают в момент инцидента
|
||||
|
||||
---
|
||||
|
||||
## Пост:
|
||||
|
||||
5 причин потери архива видеонаблюдения.
|
||||
|
||||
---
|
||||
|
||||
## Короткое видео:
|
||||
|
||||
“Почему регистратор — не гарантия записи.”
|
||||
|
||||
---
|
||||
|
||||
## Кейс:
|
||||
|
||||
“На объекте архив не записывался 4 месяца.”
|
||||
|
||||
---
|
||||
|
||||
# Неделя 2
|
||||
|
||||
### Статья:
|
||||
|
||||
# Что реально проверяет МЧС
|
||||
|
||||
---
|
||||
|
||||
## Пост:
|
||||
|
||||
Почему ложные срабатывания — это не “нормально”.
|
||||
|
||||
---
|
||||
|
||||
## Видео:
|
||||
|
||||
3 ошибки обслуживания пожарки.
|
||||
|
||||
---
|
||||
|
||||
# Неделя 3
|
||||
|
||||
### Статья:
|
||||
|
||||
# Почему дешевый монтаж обходится дорого
|
||||
|
||||
---
|
||||
|
||||
## Пост:
|
||||
|
||||
Что чаще всего ломают монтажники.
|
||||
|
||||
---
|
||||
|
||||
## Фото-кейс:
|
||||
|
||||
“Как выглядит отсутствие эксплуатации через 2 года.”
|
||||
|
||||
---
|
||||
|
||||
# Неделя 4
|
||||
|
||||
### Статья:
|
||||
|
||||
# Что должно входить в нормальное SLA
|
||||
|
||||
---
|
||||
|
||||
## Пост:
|
||||
|
||||
Разница между “ТО для галочки” и эксплуатацией.
|
||||
|
||||
---
|
||||
|
||||
## Видео:
|
||||
|
||||
“Почему подрядчики исчезают после монтажа.”
|
||||
|
||||
---
|
||||
|
||||
# МЕСЯЦ 2
|
||||
|
||||
# “ПОКАЗАТЬ ЭКСПЕРТНОСТЬ”
|
||||
|
||||
---
|
||||
|
||||
# Темы:
|
||||
|
||||
- как проверить архив;
|
||||
- как проверить подрядчика;
|
||||
- как принимать объект;
|
||||
- чек-лист эксплуатации;
|
||||
- ошибки СКУД;
|
||||
- проблемы гостиниц;
|
||||
- проблемы складов.
|
||||
|
||||
---
|
||||
|
||||
# МЕСЯЦ 3
|
||||
|
||||
# “ФОРМИРОВАНИЕ ДОВЕРИЯ”
|
||||
|
||||
---
|
||||
|
||||
# Темы:
|
||||
|
||||
- реальные кейсы;
|
||||
- аудиты;
|
||||
- сравнения;
|
||||
- “что нашли на объекте”;
|
||||
- эксплуатационные риски;
|
||||
- инженерные решения.
|
||||
|
||||
---
|
||||
|
||||
# КАК ЧАСТО ПУБЛИКОВАТЬ
|
||||
|
||||
---
|
||||
|
||||
# ИДЕАЛЬНО:
|
||||
|
||||
## 2–3 поста в неделю.
|
||||
|
||||
---
|
||||
|
||||
# НЕ НУЖНО:
|
||||
|
||||
- каждый день;
|
||||
- “активность ради активности”.
|
||||
|
||||
---
|
||||
|
||||
# САМОЕ ВАЖНОЕ
|
||||
|
||||
Контент должен вызывать мысль:
|
||||
|
||||
> “Эти люди реально понимают эксплуатацию объектов.”
|
||||
|
||||
---
|
||||
|
||||
# ЧТО ДАСТ РЕЗУЛЬТАТ БЫСТРЕЕ ВСЕГО
|
||||
|
||||
---
|
||||
|
||||
# 1. АУДИТЫ
|
||||
|
||||
---
|
||||
|
||||
# 2. КЕЙСЫ
|
||||
|
||||
---
|
||||
|
||||
# 3. РАЗБОРЫ ОШИБОК
|
||||
|
||||
---
|
||||
|
||||
# 4. УЗКАЯ НИША
|
||||
|
||||
Например:
|
||||
|
||||
- гостиницы;
|
||||
- склады.
|
||||
|
||||
---
|
||||
|
||||
# 5. ЛИЧНЫЙ БРЕНД ИНЖЕНЕРА
|
||||
|
||||
Вот это критично.
|
||||
|
||||
---
|
||||
|
||||
# ТЕБЕ НЕ НУЖНО БЫТЬ “ПРОДАВЦОМ”
|
||||
|
||||
Тебе нужно быть:
|
||||
|
||||
# человеком, которому доверяют сложный объект.
|
||||
|
||||
---
|
||||
|
||||
# ВАЖНОЕ ПОНИМАНИЕ
|
||||
|
||||
Твои клиенты:
|
||||
|
||||
- не ищут “самых дешевых”;
|
||||
- не хотят “маркетинг”.
|
||||
|
||||
Они хотят:
|
||||
|
||||
# “нормального ответственного подрядчика”.
|
||||
|
||||
И рынок сейчас чудовищно голоден на таких людей.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
||||
Ниже — тебе не “скрипты продаж”, а инженерная система входа в аудит, адаптированная под Краснодарский край и B2B-реальность (УК, гостиницы, склады, собственники).
|
||||
|
||||
Главная идея:
|
||||
|
||||
> Ты не продаёшь аудит. Ты предлагаешь проверку рисков объекта.
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. ПСИХОЛОГИЯ ПРОДАЖ АУДИТА (ВАЖНО)
|
||||
|
||||
Что продаёт аудит на самом деле:
|
||||
|
||||
снижение рисков проверок (МЧС, внутренние, страховые)
|
||||
|
||||
уверенность, что система реально работает
|
||||
|
||||
снятие ответственности с менеджера/собственника
|
||||
|
||||
“независимое мнение инженера”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Что НЕ работает:
|
||||
|
||||
“проверим камеры”
|
||||
|
||||
“сделаем диагностику”
|
||||
|
||||
“обслуживание дешевле”
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Рабочая позиция:
|
||||
|
||||
> “Мы делаем техническую проверку систем безопасности объекта с выдачей инженерного заключения по рискам эксплуатации.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. ВХОД В ПРОДАЖУ (3 КАНАЛА)
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.1 Холодный вход (самый важный в регионе)
|
||||
|
||||
Кому пишем:
|
||||
|
||||
гостиницы
|
||||
|
||||
апарт-отели
|
||||
|
||||
склады
|
||||
|
||||
УК
|
||||
|
||||
коммерческая недвижимость
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.2 Тёплый вход (через знакомства)
|
||||
|
||||
электрики
|
||||
|
||||
IT
|
||||
|
||||
строители
|
||||
|
||||
подрядчики
|
||||
|
||||
проектировщики
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.3 Вход через контент
|
||||
|
||||
Telegram / статьи / кейсы
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. СТРУКТУРА ПРОДАЖИ АУДИТА
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. Контакт
|
||||
2. Короткое позиционирование
|
||||
3. Выявление боли
|
||||
4. Предложение аудита
|
||||
5. Подтверждение логики
|
||||
6. Назначение выезда
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ХОЛОДНЫЙ СКРИПТ (ТЕКСТ + WA / TG)
|
||||
|
||||
Вариант 1 — максимально инженерный (основной)
|
||||
|
||||
Добрый день.
|
||||
|
||||
Мы занимаемся техническим сопровождением систем безопасности коммерческих объектов в Краснодарском крае.
|
||||
|
||||
Перед тем как брать объекты на обслуживание, обычно делаем технический аудит:
|
||||
— проверка видеонаблюдения (архив, запись, питание)
|
||||
— проверка СКУД
|
||||
— проверка пожарной сигнализации
|
||||
— оценка рисков эксплуатации
|
||||
|
||||
По практике, на большинстве объектов выявляются скрытые проблемы, которые не видны в ежедневной работе.
|
||||
|
||||
Если у вас есть задача понять текущее состояние систем — можем провести аудит и дать инженерное заключение по рискам.
|
||||
|
||||
Подскажите, у вас сейчас системы на обслуживании или “как есть работают”?
|
||||
|
||||
|
||||
---
|
||||
|
||||
Почему это работает:
|
||||
|
||||
нет продажи
|
||||
|
||||
есть диагностика
|
||||
|
||||
есть статус эксперта
|
||||
|
||||
есть вопрос (диалог)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. ВТОРОЙ СООБЩЕНИЕ (если отвечают “работает/есть подрядчик”)
|
||||
|
||||
Понял.
|
||||
|
||||
В таких случаях чаще всего проблема не в “работает/не работает”, а в том, что:
|
||||
— архив не проверяется регулярно
|
||||
— резервирование не тестируется
|
||||
— часть оборудования работает нестабильно
|
||||
— нет объективного контроля состояния
|
||||
|
||||
А это всплывает обычно в момент инцидента или проверки.
|
||||
|
||||
Мы как раз делаем аудит, который показывает такие скрытые вещи до проблем.
|
||||
|
||||
Если интересно — можем сделать экспресс-проверку объекта с коротким отчётом по рискам.
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. КОНВЕРСИЯ В АУДИТ (КЛЮЧЕВОЙ МОМЕНТ)
|
||||
|
||||
Можем сделать так:
|
||||
|
||||
— выезд инженера
|
||||
— проверка всех систем
|
||||
— фиксация состояния
|
||||
— короткий инженерный отчёт
|
||||
— список рисков и рекомендаций
|
||||
|
||||
Обычно это занимает 1–2 дня в зависимости от объекта.
|
||||
|
||||
По стоимости — от 15 до 50 тыс, зависит от объёма систем.
|
||||
|
||||
Если удобно — могу уточнить объект и сказать точнее по формату.
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. СКРИПТ ДЛЯ ГОЛОСА (ЕСЛИ ЗВОНЯТ)
|
||||
|
||||
структура:
|
||||
|
||||
1. Позиционирование (10–15 сек)
|
||||
|
||||
Мы занимаемся техническим сопровождением систем безопасности коммерческих объектов.
|
||||
Перед обслуживанием всегда делаем аудит состояния систем.
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. Боль
|
||||
|
||||
На большинстве объектов, которые мы смотрим, есть скрытые проблемы — архив, питание, доступ, пожарка.
|
||||
Обычно это не видно в ежедневной работе.
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. Переход
|
||||
|
||||
Поэтому предлагаем сначала сделать техническую проверку объекта и дать инженерное заключение.
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. Закрытие
|
||||
|
||||
Если вам актуально — можем согласовать выезд инженера и формат проверки.
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. ОБРАБОТКА ВОЗРАЖЕНИЙ
|
||||
|
||||
|
||||
---
|
||||
|
||||
❌ “У нас уже есть подрядчик”
|
||||
|
||||
Понял.
|
||||
|
||||
Аудит как раз часто показывает, что объект обслуживается формально, но без проверки ключевых параметров:
|
||||
архив, резервирование, ошибки, питание.
|
||||
|
||||
Мы не конкурируем с подрядчиком — мы даём независимую оценку состояния систем.
|
||||
|
||||
|
||||
---
|
||||
|
||||
❌ “Дорого”
|
||||
|
||||
Аудит не про “сделать дешевле”.
|
||||
|
||||
Это проверка рисков, которые могут стоить дороже:
|
||||
— потеря архива
|
||||
— отказ системы
|
||||
— проблемы при проверках
|
||||
|
||||
Поэтому стоимость зависит от масштаба объекта, но обычно это разовая диагностика, а не постоянные расходы.
|
||||
|
||||
|
||||
---
|
||||
|
||||
❌ “Нам не нужно”
|
||||
|
||||
Понял.
|
||||
|
||||
На практике аудит нужен не всегда “сейчас”, а когда появляются сомнения в стабильности систем или перед проверками.
|
||||
|
||||
Если актуализируется — можем подключиться.
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. СИСТЕМА ДОГРЕВА (ЕСЛИ НЕ КУПИЛИ СРАЗУ)
|
||||
|
||||
Через 3–5 дней:
|
||||
|
||||
Добрый день.
|
||||
|
||||
Подскажите, актуально ли сейчас техническое обследование систем безопасности?
|
||||
|
||||
По опыту, на объектах часто всплывают вопросы по архиву и резервированию, которые лучше проверить заранее, чем в момент инцидента.
|
||||
|
||||
|
||||
---
|
||||
|
||||
10. МИКРО-CRM ЛОГИКА (ВАЖНО)
|
||||
|
||||
Каждый контакт делишь на:
|
||||
|
||||
не ответил
|
||||
|
||||
думает
|
||||
|
||||
есть подрядчик
|
||||
|
||||
отказ
|
||||
|
||||
сделал аудит
|
||||
|
||||
ушёл в SLA
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
11. КЛЮЧЕВОЙ МЕХАНИЗМ ПРОДАЖ
|
||||
|
||||
Твоя система должна работать так:
|
||||
|
||||
контакт → инженерная диагностика → выявление рисков → логичный переход в SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
12. ПОЧЕМУ ЭТО РАБОТАЕТ ИМЕННО В КРАСНОДАРСКОМ КРАЕ
|
||||
|
||||
Рынок региона:
|
||||
|
||||
много частных объектов
|
||||
|
||||
слабая эксплуатация систем
|
||||
|
||||
подрядчики “формальные”
|
||||
|
||||
мало инженерных компаний уровня SLA
|
||||
|
||||
|
||||
👉 значит: ты не конкурируешь по цене — ты создаёшь новый тип услуги
|
||||
|
||||
|
||||
---
|
||||
|
||||
13. ЕСЛИ ДАЛЬШЕ УСИЛИВАТЬ СИСТЕМУ
|
||||
|
||||
Следующий уровень, который я могу тебе собрать:
|
||||
|
||||
скрипт “закрытия в SLA после аудита”
|
||||
|
||||
таблица квалификации клиента (кого брать/кого нет)
|
||||
|
||||
CRM-воронка под SLA (по стадиям)
|
||||
|
||||
шаблон отчёта аудита (который продаёт сам себя)
|
||||
|
||||
шаблон договора SLA (который не торгуется)
|
||||
|
||||
|
||||
И это уже будет не продажи.
|
||||
|
||||
Это будет:
|
||||
|
||||
инженерная коммерческая система повторяемого дохода.
|
||||
@@ -0,0 +1,556 @@
|
||||
Сейчас мы выходим на ключевой этап.
|
||||
Потому что:
|
||||
|
||||
УТП — это не “красивый текст”.
|
||||
|
||||
Это:
|
||||
|
||||
твоя позиция на рынке.
|
||||
|
||||
Именно здесь определяется:
|
||||
|
||||
будешь ли ты конкурировать ценой;
|
||||
|
||||
попадешь ли в “монтажников”;
|
||||
|
||||
или станешь инженерным B2B подрядчиком.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ГЛАВНАЯ ОШИБКА РЫНКА
|
||||
|
||||
Все пишут:
|
||||
|
||||
“качественный монтаж”;
|
||||
|
||||
“20 лет опыта”;
|
||||
|
||||
“индивидуальный подход”.
|
||||
|
||||
|
||||
Это не УТП.
|
||||
|
||||
Это информационный шум.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТВОЕ РЕАЛЬНОЕ ПРЕИМУЩЕСТВО
|
||||
|
||||
Ты НЕ:
|
||||
|
||||
продавец оборудования;
|
||||
|
||||
бригада монтажников;
|
||||
|
||||
дешевый подрядчик.
|
||||
|
||||
|
||||
Ты:
|
||||
|
||||
инженерная сервисная компания.
|
||||
|
||||
И продавать нужно:
|
||||
|
||||
ответственность;
|
||||
|
||||
стабильность;
|
||||
|
||||
контроль;
|
||||
|
||||
снижение рисков;
|
||||
|
||||
эксплуатацию;
|
||||
|
||||
прозрачность.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТВОЕ ОСНОВНОЕ УТП
|
||||
|
||||
Вот сильная база.
|
||||
|
||||
|
||||
---
|
||||
|
||||
AEGISONE — инженерное сопровождение систем безопасности коммерческих объектов.
|
||||
|
||||
Мы не просто устанавливаем оборудование.
|
||||
Мы берем на себя ответственность за стабильную работу систем безопасности объекта.
|
||||
|
||||
Работаем с коммерческой недвижимостью, гостиницами, складами, производствами и сетевыми объектами.
|
||||
|
||||
Обеспечиваем:
|
||||
— регламентное обслуживание;
|
||||
— SLA и контроль сроков реакции;
|
||||
— технический аудит;
|
||||
— восстановление проблемных объектов;
|
||||
— сопровождение эксплуатации;
|
||||
— прозрачную отчетность и контроль инфраструктуры.
|
||||
|
||||
Если система безопасности нужна не «для галочки», а для реальной работы объекта — мы подходим.
|
||||
---
|
||||
|
||||
ПОЧЕМУ ЭТО СИЛЬНО
|
||||
|
||||
Тут нет:
|
||||
|
||||
“лучших цен”;
|
||||
|
||||
“монтажа под ключ”;
|
||||
|
||||
“любых работ”.
|
||||
|
||||
|
||||
Это:
|
||||
|
||||
позиция эксперта.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ДОПОЛНИТЕЛЬНЫЕ УТП ДЛЯ РАЗНЫХ СЕГМЕНТОВ
|
||||
|
||||
|
||||
---
|
||||
|
||||
ДЛЯ ГОСТИНИЦ
|
||||
|
||||
Обеспечиваем стабильную работу систем безопасности гостиниц и апарт-отелей без постоянного контроля со стороны управляющего.
|
||||
|
||||
Берем на себя техническое сопровождение:
|
||||
— видеонаблюдения;
|
||||
— СКУД;
|
||||
— пожарной сигнализации;
|
||||
— сетевой инфраструктуры.
|
||||
|
||||
Работаем по SLA и регламентам.
|
||||
---
|
||||
|
||||
ДЛЯ СКЛАДОВ И ЛОГИСТИКИ
|
||||
|
||||
Снижаем риски потери архива, отказов систем и простоев инфраструктуры складских объектов.
|
||||
|
||||
Контролируем работоспособность:
|
||||
— видеонаблюдения;
|
||||
— контроля доступа;
|
||||
— периметра;
|
||||
— сетевой инфраструктуры.
|
||||
|
||||
Обеспечиваем регламентное обслуживание и аварийное реагирование.
|
||||
---
|
||||
|
||||
ТЕПЕРЬ САМОЕ ВАЖНОЕ
|
||||
|
||||
СТРУКТУРА КОММЕРЧЕСКОГО ПРЕДЛОЖЕНИЯ
|
||||
|
||||
Большинство КП на рынке — мусор.
|
||||
|
||||
Там:
|
||||
|
||||
список оборудования;
|
||||
|
||||
цены;
|
||||
|
||||
таблицы;
|
||||
|
||||
“надежные решения”.
|
||||
|
||||
|
||||
B2B это НЕ читает.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТВОЕ КП ДОЛЖНО ПРОДАВАТЬ:
|
||||
|
||||
СПОКОЙСТВИЕ И КОНТРОЛЬ.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ИДЕАЛЬНАЯ СТРУКТУРА КП ДЛЯ ТЕБЯ
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. ТИТУЛЬНАЯ СТРАНИЦА
|
||||
|
||||
|
||||
---
|
||||
|
||||
Заголовок:
|
||||
|
||||
Коммерческое предложение
|
||||
|
||||
по техническому сопровождению систем безопасности
|
||||
|
||||
|
||||
---
|
||||
|
||||
Подзаголовок:
|
||||
|
||||
Для:
|
||||
|
||||
гостиницы;
|
||||
|
||||
склада;
|
||||
|
||||
коммерческого объекта;
|
||||
|
||||
производства.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Внизу:
|
||||
|
||||
логотип;
|
||||
|
||||
контакты;
|
||||
|
||||
дата.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. КРАТКО О ПРОБЛЕМЕ
|
||||
|
||||
Это критически важно.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
|
||||
---
|
||||
|
||||
Большинство неисправностей систем безопасности выявляются только после возникновения проблем:
|
||||
— отсутствует архив видеонаблюдения;
|
||||
— часть оборудования не функционирует;
|
||||
— отсутствует документация;
|
||||
— подрядчики не несут ответственности;
|
||||
— неисправности накапливаются месяцами.
|
||||
|
||||
Это приводит к рискам простоев, потере контроля и проблемам при проверках.
|
||||
|
||||
Для снижения этих рисков необходимо регулярное техническое сопровождение и контроль состояния инфраструктуры объекта.
|
||||
---
|
||||
|
||||
ПОЧЕМУ ЭТО СИЛЬНО
|
||||
|
||||
Ты:
|
||||
|
||||
не “продаешь услуги”;
|
||||
|
||||
а показываешь проблему бизнеса.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. ЧТО ТЫ ПРЕДЛАГАЕШЬ
|
||||
|
||||
|
||||
---
|
||||
|
||||
Заголовок:
|
||||
|
||||
Решение
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
|
||||
---
|
||||
|
||||
AEGISONE обеспечивает техническое сопровождение и контроль работоспособности систем безопасности объекта.
|
||||
|
||||
Мы берем на себя:
|
||||
— регламентное обслуживание;
|
||||
— диагностику;
|
||||
— аварийное реагирование;
|
||||
— технический контроль;
|
||||
— сопровождение эксплуатации;
|
||||
— контроль подрядчиков;
|
||||
— отчетность и рекомендации.
|
||||
---
|
||||
|
||||
4. ЧТО ВХОДИТ В ОБСЛУЖИВАНИЕ
|
||||
|
||||
Вот здесь уже конкретика.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример структуры:
|
||||
|
||||
Услуга Описание
|
||||
|
||||
Регламентные проверки Проверка состояния оборудования
|
||||
Контроль архива Проверка записи и хранения
|
||||
Аварийные выезды Реагирование по SLA
|
||||
Диагностика Поиск неисправностей
|
||||
Отчетность Фото и рекомендации
|
||||
Поддержка Консультации персонала
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. SLA
|
||||
|
||||
Очень важно.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
Приоритет Время реакции
|
||||
|
||||
Критическая авария до 2 часов
|
||||
Частичный отказ до 4 часов
|
||||
Плановая заявка до 24 часов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. ПОЧЕМУ ИМЕННО ВЫ
|
||||
|
||||
Вот тут нельзя писать банальности.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПРАВИЛЬНО:
|
||||
|
||||
|
||||
---
|
||||
|
||||
Почему заказчики работают с нами:
|
||||
|
||||
— более 20 лет инженерного опыта;
|
||||
— лицензия МЧС;
|
||||
— работа по регламентам и SLA;
|
||||
— прозрачная отчетность;
|
||||
— опыт эксплуатации сложных объектов;
|
||||
— несем ответственность за результат;
|
||||
— не работаем по принципу «сделали и забыли».
|
||||
---
|
||||
|
||||
7. СТОИМОСТЬ
|
||||
|
||||
Очень важный блок.
|
||||
|
||||
|
||||
---
|
||||
|
||||
НЕ ДЕЛАЙ:
|
||||
|
||||
“от 5000 ₽”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПРАВИЛЬНО:
|
||||
|
||||
Тариф Формат Стоимость
|
||||
|
||||
START Небольшие объекты от 25 000 ₽
|
||||
BUSINESS Коммерческие объекты от 60 000 ₽
|
||||
ENTERPRISE Внешний инженерный отдел индивидуально
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. ЭТАПЫ РАБОТЫ
|
||||
|
||||
Это резко повышает доверие.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
1. Технический аудит объекта
|
||||
|
||||
|
||||
2. Формирование регламентов
|
||||
|
||||
|
||||
3. Подписание SLA
|
||||
|
||||
|
||||
4. Постановка объекта на сопровождение
|
||||
|
||||
|
||||
5. Регулярное обслуживание и отчетность
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. CTA
|
||||
|
||||
Очень важно.
|
||||
|
||||
|
||||
---
|
||||
|
||||
НЕ:
|
||||
|
||||
“оставьте заявку”.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ПРАВИЛЬНО:
|
||||
|
||||
Предлагаем провести технический аудит объекта и подготовить рекомендации по снижению эксплуатационных рисков.
|
||||
|
||||
|
||||
---
|
||||
|
||||
КАК ДОЛЖНО ВЫГЛЯДЕТЬ КП ВИЗУАЛЬНО
|
||||
|
||||
|
||||
---
|
||||
|
||||
НЕ:
|
||||
|
||||
“цветастая реклама”;
|
||||
|
||||
“маркетинговый шум”.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
А:
|
||||
|
||||
спокойный инженерный документ.
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЦВЕТА
|
||||
|
||||
темно-синий;
|
||||
|
||||
графит;
|
||||
|
||||
белый;
|
||||
|
||||
серый.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
СТИЛЬ
|
||||
|
||||
Как:
|
||||
|
||||
техническая компания;
|
||||
|
||||
инженерный аудит;
|
||||
|
||||
эксплуатация объектов.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
САМОЕ ВАЖНОЕ
|
||||
|
||||
Твое КП должно вызывать мысль:
|
||||
|
||||
> “Эти люди понимают, как работает объект.”
|
||||
|
||||
|
||||
|
||||
А не:
|
||||
|
||||
> “Очередные монтажники.”
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
И ТУТ КЛЮЧЕВОЙ МОМЕНТ
|
||||
|
||||
Ты не должен пытаться понравиться всем.
|
||||
|
||||
Тебе нужны:
|
||||
|
||||
нормальные объекты;
|
||||
|
||||
нормальные бюджеты;
|
||||
|
||||
нормальные заказчики.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ТВОЯ СТРАТЕГИЯ
|
||||
|
||||
Не:
|
||||
|
||||
“много дешевых клиентов”.
|
||||
|
||||
А:
|
||||
|
||||
“мало, но качественных объектов на абонентке”.
|
||||
|
||||
Это огромная разница в бизнес-модели.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Следующий логичный этап:
|
||||
|
||||
1. Сделать полноценный шаблон КП (PDF-структуру).
|
||||
|
||||
|
||||
2. Прописать тарифы глубже.
|
||||
|
||||
|
||||
3. Сделать SLA-документ.
|
||||
|
||||
|
||||
4. Подготовить скрипт инженерной продажи.
|
||||
|
||||
|
||||
5. Сделать структуру первого аудита.
|
||||
|
||||
|
||||
6. Составить чек-листы.
|
||||
|
||||
|
||||
7. Построить первую воронку клиентов в Краснодарском крае.
|
||||
|
||||
|
||||
8. Подготовить SEO-структуру сайта.
|
||||
|
||||
|
||||
9. Разработать контент-план для выхода в экспертность.
|
||||
|
||||
|
||||
10. Построить модель роста до 1–3 млн/мес recurring revenue.
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Формулы и расчёты AegisOne
|
||||
|
||||
## 1. Risk Score (Оценка риска объекта)
|
||||
|
||||
**Формула:** Сумма весов выявленных нарушений
|
||||
|
||||
| Нарушение | Коэффициент (по умолч.) |
|
||||
|-----------|------------------------|
|
||||
| Нет архива видеонаблюдения | 25 |
|
||||
| Нет резервного питания | 20 |
|
||||
| Нет регламента обслуживания | 15 |
|
||||
| Частые сбои систем | 20 |
|
||||
| Нет документации | 10 |
|
||||
|
||||
**Максимум:** 100 баллов
|
||||
|
||||
**Интерпретация:**
|
||||
- 0–20: Низкий
|
||||
- 21–50: Средний
|
||||
- 51–75: Высокий
|
||||
- 76–100: Критический
|
||||
|
||||
---
|
||||
|
||||
## 2. Complexity Index (Сложность обслуживания)
|
||||
|
||||
**Формула:** Сумма баллов по компонентам
|
||||
|
||||
| Компонент | Значение | Коэффициент |
|
||||
|-----------|----------|-------------|
|
||||
| Камеры | до 20 | 10 |
|
||||
| Камеры | 20–100 | 20 |
|
||||
| Камеры | 100+ | 35 |
|
||||
| Точки СКУД | до 5 | 10 |
|
||||
| Точки СКУД | 5–20 | 20 |
|
||||
| Точки СКУД | 20+ | 30 |
|
||||
| Пожарная | Простая | 15 |
|
||||
| Пожарная | Средняя | 25 |
|
||||
| Пожарная | Сложная (>5000 м²) | 40 |
|
||||
| IT-инфраструктура | Есть | 10 |
|
||||
|
||||
**Интерпретация:**
|
||||
- до 30: Простой
|
||||
- 31–70: Средний
|
||||
- 71–120: Сложный
|
||||
- 120+: Enterprise
|
||||
|
||||
---
|
||||
|
||||
## 3. Infrastructure Load (Нагрузка на инфраструктуру)
|
||||
|
||||
| Параметр | Состояние | Коэффициент |
|
||||
|----------|-----------|-------------|
|
||||
| Сервер | Нет | 15 |
|
||||
| Сервер | Слабый | 10 |
|
||||
| Сеть | Нестабильная | 20 |
|
||||
| Сеть | Частично | 10 |
|
||||
| UPS | Нет | 20 |
|
||||
| UPS | Слабый | 10 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Service History (История обслуживания)
|
||||
|
||||
| Состояние | Коэффициент |
|
||||
|-----------|-------------|
|
||||
| Не обслуживается | 30 |
|
||||
| Нерегулярно | 20 |
|
||||
| Формальный подрядчик | 10 |
|
||||
| Есть SLA | 0 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Object Index (Итоговый индекс объекта)
|
||||
|
||||
**Формула:**
|
||||
```
|
||||
Object Index = Risk × 0.4 + Complexity × 0.3 + Infra × 0.2 + History × 0.1
|
||||
```
|
||||
|
||||
**Классы:**
|
||||
- A (до 30): Лёгкий SLA
|
||||
- B (31–60): Стандарт SLA
|
||||
- C (61–90): Сложный SLA
|
||||
- D (90+): Enterprise / Высокий риск
|
||||
|
||||
---
|
||||
|
||||
## 6. SLA Price (Стоимость SLA)
|
||||
|
||||
**Формула:**
|
||||
```
|
||||
SLA Price = Base Cost × Object Index × Region Factor × Risk Multiplier
|
||||
```
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Base Cost | 15 000 ₽ |
|
||||
| Region Factor | Краснодар 1.0 / Край 1.1 / РФ 1.3 / Удалённые 1.6 / Москва 1.8 |
|
||||
|
||||
**Risk Multiplier:**
|
||||
- Низкий риск: 1.0
|
||||
- Средний: 1.3
|
||||
- Высокий: 1.6
|
||||
- Критический: 2.0
|
||||
|
||||
---
|
||||
|
||||
## 7. Engineer Score (ES)
|
||||
|
||||
**Формула:** Взвешенная сумма компонентов
|
||||
|
||||
| Компонент | Вес |
|
||||
|-----------|-----|
|
||||
| SLA Compliance | 0.25 |
|
||||
| Response Time Score | 0.20 |
|
||||
| Resolution Time Score | 0.20 |
|
||||
| Diagnosis Accuracy | 0.15 |
|
||||
| Reopen Rate Score | 0.10 |
|
||||
| Risk Coverage Score | 0.10 |
|
||||
|
||||
**Грейды:**
|
||||
- 90+: Senior
|
||||
- 80–89: Strong
|
||||
- 70–79: Middle
|
||||
- <70: Junior
|
||||
|
||||
---
|
||||
|
||||
## 8. SHS (System Health Score)
|
||||
|
||||
**Формула:**
|
||||
```
|
||||
SHS = SLA Stability × 0.22 + Revenue Stability × 0.18 + Retention × 0.18
|
||||
+ Engineer Performance × 0.15 + Incident Stability × 0.12
|
||||
+ Sales Flow × 0.10 + Operational Efficiency × 0.05
|
||||
```
|
||||
|
||||
**Статусы:**
|
||||
- 85+: Growth
|
||||
- 70–84: Stable
|
||||
- 50–69: Risk
|
||||
- <50: Crisis
|
||||
|
||||
---
|
||||
|
||||
## Автоматические правила
|
||||
|
||||
| Условие | Действие |
|
||||
|---------|----------|
|
||||
| SHS < 70 | Снизить продажи, запустить аудит, пересмотреть назначения |
|
||||
| SLA Compliance < 90% | Заморозить неприоритетные проекты, увеличить частоту проверок |
|
||||
| Retention < 90% | Обязательный аудит клиентов, пересмотр назначений |
|
||||
|
||||
---
|
||||
|
||||
Все коэффициенты можно изменить в разделе **Коэффициенты** админ-панели. Изменения применяются к новым расчётам.
|
||||
@@ -0,0 +1,458 @@
|
||||
Ниже — единая цифровая операционная модель AegisOne Engineering в формате .md.
|
||||
|
||||
Это не просто документация — это “скелет системы”, который можно отдать разработчику, внедрить в Notion/Bitrix/1C/веб-CRM или развернуть как внутренний продукт.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# AegisOne Engineering
|
||||
## Unified Digital Operating System (UDOS)
|
||||
|
||||
---
|
||||
|
||||
# 0. СУТЬ СИСТЕМЫ
|
||||
|
||||
AegisOne UDOS — это единая цифровая система управления инженерной эксплуатацией объектов безопасности.
|
||||
|
||||
Она объединяет:
|
||||
|
||||
- CRM (клиенты и сделки)
|
||||
- SLA (контракты и обслуживание)
|
||||
- KPI инженеров (исполнение)
|
||||
- SHS (здоровье бизнеса)
|
||||
- Финансовую модель
|
||||
- Операционный дашборд
|
||||
|
||||
---
|
||||
|
||||
# 1. АРХИТЕКТУРА СИСТЕМЫ
|
||||
|
||||
┌──────────────────────┐
|
||||
│ LEADS / CRM │
|
||||
└─────────┬────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ AUDIT SYSTEM │
|
||||
└─────────┬────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ SLA CONTRACTS │
|
||||
└─────────┬────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ OPERATIONS (ENGINEERS) │
|
||||
└─────────┬───────────────┬──────────────┘
|
||||
↓ ↓
|
||||
┌──────────────┐ ┌─────────────────────┐
|
||||
│ KPI ENGINEERS │ │ INCIDENT SYSTEM │
|
||||
└──────┬───────┘ └─────────┬───────────┘
|
||||
↓ ↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ SHS ENGINE │
|
||||
└──────────────┬──────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CEO DASHBOARD │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
---
|
||||
|
||||
# 2. CRM СИСТЕМА
|
||||
|
||||
## 2.1 Структура клиента
|
||||
|
||||
```json
|
||||
Client {
|
||||
id,
|
||||
company_name,
|
||||
object_type,
|
||||
location,
|
||||
contact_person,
|
||||
decision_maker,
|
||||
number_of_objects,
|
||||
systems: {
|
||||
video: int,
|
||||
access_control: int,
|
||||
fire_alarm: bool,
|
||||
it_infrastructure: bool
|
||||
},
|
||||
current_provider,
|
||||
pain_points[],
|
||||
budget_level,
|
||||
status: [lead, qualified, audit, offer, sla, lost]
|
||||
}
|
||||
|
||||
|
||||
---
|
||||
|
||||
2.2 Воронка CRM
|
||||
|
||||
Lead
|
||||
↓
|
||||
Qualification
|
||||
↓
|
||||
Technical Audit
|
||||
↓
|
||||
Risk Report
|
||||
↓
|
||||
SLA Offer
|
||||
↓
|
||||
Contract
|
||||
↓
|
||||
Active SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. AUDIT SYSTEM (ИНЖЕНЕРНЫЙ ВХОД)
|
||||
|
||||
3.1 Формирование Risk Score
|
||||
|
||||
Risk Score =
|
||||
missing_archive (25)
|
||||
+ no_power_backup (20)
|
||||
+ no_regulations (15)
|
||||
+ system_failures (20)
|
||||
+ no_documentation (10)
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.2 Output аудита
|
||||
|
||||
Audit Report:
|
||||
- Risk Score
|
||||
- System condition
|
||||
- Critical vulnerabilities
|
||||
- SLA recommendation
|
||||
- Cost estimation
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. SLA СИСТЕМА
|
||||
|
||||
4.1 Формула стоимости
|
||||
|
||||
SLA Price =
|
||||
Base Cost × Object Index × Region Factor × Risk Multiplier
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.2 Object Index
|
||||
|
||||
Object Index =
|
||||
(Risk × 0.4)
|
||||
+ (Complexity × 0.3)
|
||||
+ (Infrastructure × 0.2)
|
||||
+ (Service History × 0.1)
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.3 SLA уровни
|
||||
|
||||
Class Index Description
|
||||
|
||||
A 0–30 simple SLA
|
||||
B 31–60 standard SLA
|
||||
C 61–90 complex SLA
|
||||
D 90+ enterprise SLA
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4.4 SLA метрики
|
||||
|
||||
Response Time
|
||||
|
||||
Resolution Time
|
||||
|
||||
Uptime %
|
||||
|
||||
Incident Rate
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. KPI СИСТЕМА ИНЖЕНЕРОВ
|
||||
|
||||
5.1 Engineer Score
|
||||
|
||||
ES =
|
||||
(0.25 × SLA Compliance)
|
||||
+ (0.20 × Response Time Score)
|
||||
+ (0.20 × Resolution Time Score)
|
||||
+ (0.15 × Diagnosis Accuracy)
|
||||
+ (0.10 × Reopen Rate Score)
|
||||
+ (0.10 × Documentation Quality)
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.2 KPI метрики
|
||||
|
||||
SLA Compliance
|
||||
|
||||
closed_in_SLA / total_requests
|
||||
|
||||
Reopen Rate
|
||||
|
||||
reopened_requests / total_requests
|
||||
|
||||
Diagnosis Accuracy
|
||||
|
||||
confirmed_faults / found_faults
|
||||
|
||||
Utilization
|
||||
|
||||
working_hours / available_hours
|
||||
|
||||
|
||||
---
|
||||
|
||||
5.3 Грейды инженеров
|
||||
|
||||
Score Level
|
||||
|
||||
90–100 Senior
|
||||
80–89 Strong
|
||||
70–79 Middle
|
||||
<70 Junior
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. SHS (SYSTEM HEALTH SCORE)
|
||||
|
||||
6.1 Формула SHS
|
||||
|
||||
SHS =
|
||||
(0.22 × SLA Stability)
|
||||
+ (0.18 × Revenue Stability)
|
||||
+ (0.18 × Retention)
|
||||
+ (0.15 × Engineer Performance)
|
||||
+ (0.12 × Incident Stability)
|
||||
+ (0.10 × Sales Flow)
|
||||
+ (0.05 × Operational Efficiency)
|
||||
|
||||
|
||||
---
|
||||
|
||||
6.2 Интерпретация
|
||||
|
||||
SHS Status
|
||||
|
||||
85–100 Growth
|
||||
70–85 Stable
|
||||
50–70 Risk
|
||||
<50 Crisis
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6.3 Alert System
|
||||
|
||||
SHS ↓ 5% weekly → Warning
|
||||
|
||||
SHS ↓ 10% → Critical
|
||||
|
||||
Retention drop → Immediate audit trigger
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. INCIDENT SYSTEM
|
||||
|
||||
7.1 Incident Classification
|
||||
|
||||
Level Description
|
||||
|
||||
P1 critical system failure
|
||||
P2 partial failure
|
||||
P3 minor issue
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
7.2 Incident Rate
|
||||
|
||||
IR = incidents / number_of_objects
|
||||
|
||||
|
||||
---
|
||||
|
||||
7.3 SLA breach tracking
|
||||
|
||||
breach_rate = failed_SLA_cases / total_cases
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. FINANCIAL LAYER
|
||||
|
||||
8.1 MRR
|
||||
|
||||
MRR = Σ SLA contracts / 12
|
||||
|
||||
|
||||
---
|
||||
|
||||
8.2 Revenue per Engineer
|
||||
|
||||
RPE = SLA Revenue / number_of_engineers
|
||||
|
||||
|
||||
---
|
||||
|
||||
8.3 Cost per Object
|
||||
|
||||
CPO = total_operational_cost / number_of_objects
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. CEO DASHBOARD
|
||||
|
||||
9.1 Main indicators
|
||||
|
||||
SHS (system health)
|
||||
|
||||
MRR (monthly recurring revenue)
|
||||
|
||||
SLA Compliance
|
||||
|
||||
Retention
|
||||
|
||||
Incident Rate
|
||||
|
||||
Engineer Score Avg
|
||||
|
||||
Sales Conversion
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
9.2 Dashboard visualization blocks
|
||||
|
||||
1. SHS Line Chart
|
||||
|
||||
trend over time
|
||||
|
||||
|
||||
2. SLA Heatmap
|
||||
|
||||
engineers vs performance
|
||||
|
||||
|
||||
3. MRR Growth Curve
|
||||
|
||||
monthly recurring revenue
|
||||
|
||||
|
||||
4. Client Retention Funnel
|
||||
|
||||
drop-off points
|
||||
|
||||
|
||||
5. Incident Map
|
||||
|
||||
problematic objects
|
||||
|
||||
|
||||
6. Engineer Ranking Table
|
||||
|
||||
top/bottom performers
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
10. AUTOMATION RULES
|
||||
|
||||
10.1 If SHS drops
|
||||
|
||||
IF SHS < 70:
|
||||
reduce new sales
|
||||
trigger audit cycle
|
||||
reassign engineers
|
||||
|
||||
|
||||
---
|
||||
|
||||
10.2 If SLA compliance drops
|
||||
|
||||
IF SLA < 90%:
|
||||
freeze non-critical projects
|
||||
increase inspection frequency
|
||||
|
||||
|
||||
---
|
||||
|
||||
10.3 If retention drops
|
||||
|
||||
IF retention < 90%:
|
||||
mandatory client audit
|
||||
review engineer assignments
|
||||
|
||||
|
||||
---
|
||||
|
||||
11. SYSTEM BEHAVIOR MODEL
|
||||
|
||||
DATA → KPI → SHS → DECISION → ACTION → FEEDBACK LOOP
|
||||
|
||||
|
||||
---
|
||||
|
||||
12. STRATEGIC PRINCIPLE
|
||||
|
||||
AegisOne Engineering is not a service company.
|
||||
|
||||
It is:
|
||||
|
||||
> an operating system for managing engineering risk in physical infrastructure.
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
13. END STATE MODEL
|
||||
|
||||
The system evolves into:
|
||||
|
||||
predictive SLA risk detection
|
||||
|
||||
automated pricing
|
||||
|
||||
engineer auto-allocation
|
||||
|
||||
client churn prediction
|
||||
|
||||
MRR forecasting
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
END OF SYSTEM
|
||||
|
||||
---
|
||||
|
||||
Если хочешь следующий уровень, я могу собрать тебе:
|
||||
|
||||
1) визуальный UI дашборда (как он должен выглядеть как продукт)
|
||||
2) структуру базы данных (SQL / 1C / Bitrix schema)
|
||||
3) алгоритм “предикта потери клиента за 14 дней”
|
||||
4) или автоматический генератор КП + SLA из этой системы
|
||||
|
||||
Это уже уровень **enterprise SaaS, а не просто инженерной компании**.
|
||||
@@ -0,0 +1,629 @@
|
||||
Ниже — как должна работать автоматическая система SHS (System Health Score) в твоей модели AegisOne Engineering.
|
||||
|
||||
Это уже не “аналитика бизнеса”, а:
|
||||
|
||||
> инженерная система раннего предупреждения о деградации компании
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
0. ЧТО ТАКОЕ SHS (СУТЬ СИСТЕМЫ)
|
||||
|
||||
SHS — это единый индекс здоровья бизнеса (0–100), который показывает:
|
||||
|
||||
насколько стабильно работает SLA-модель
|
||||
|
||||
где теряются деньги
|
||||
|
||||
где растут риски разрыва контрактов
|
||||
|
||||
где перегружается команда
|
||||
|
||||
где падает качество эксплуатации
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. ЛОГИКА СИСТЕМЫ SHS
|
||||
|
||||
Данные (CRM + SLA + инженеры)
|
||||
↓
|
||||
Нормализация метрик
|
||||
↓
|
||||
Взвешивание показателей
|
||||
↓
|
||||
Расчёт SHS
|
||||
↓
|
||||
Детектирование отклонений
|
||||
↓
|
||||
Сигналы риска (alerts)
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. ФИНАЛЬНАЯ ФОРМУЛА SHS
|
||||
|
||||
SHS =
|
||||
(0.22 × SLA Stability Index) +
|
||||
(0.18 × Revenue Stability Index) +
|
||||
(0.18 × Retention Index) +
|
||||
(0.15 × Engineering Performance Index) +
|
||||
(0.12 × Incident Stability Index) +
|
||||
(0.10 × Sales Flow Index) +
|
||||
(0.05 × Operational Efficiency Index)
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. РАСШИФРОВКА ВСЕХ КОМПОНЕНТОВ
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.1 SLA STABILITY INDEX (критический)
|
||||
|
||||
Смысл:
|
||||
|
||||
Насколько система выполняет SLA без сбоев.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
SSI = SLA_compliance × (1 - SLA_breach_severity)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Где:
|
||||
|
||||
SLA_compliance = выполненные заявки / все заявки
|
||||
|
||||
SLA_breach_severity = тяжесть нарушений (0–1)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
compliance = 0.96
|
||||
|
||||
нарушения = 0.1
|
||||
|
||||
|
||||
→ SSI = 0.864
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риски:
|
||||
|
||||
падение ниже 0.85 = начинается утечка клиентов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.2 REVENUE STABILITY INDEX
|
||||
|
||||
Смысл:
|
||||
|
||||
Стабильность денежного потока SLA
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
RSI = 1 - (σ(MRR) / mean(MRR))
|
||||
|
||||
|
||||
---
|
||||
|
||||
Интерпретация:
|
||||
|
||||
чем меньше колебания → тем выше индекс
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риски:
|
||||
|
||||
RSI < 0.7 → нестабильная финансовая модель
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.3 RETENTION INDEX (очень критично)
|
||||
|
||||
Формула:
|
||||
|
||||
RI = retained_clients / total_clients
|
||||
|
||||
|
||||
---
|
||||
|
||||
Дополнение:
|
||||
|
||||
штраф за уход крупных клиентов:
|
||||
|
||||
RI_adjusted = RI - (lost_key_clients × 0.1)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риски:
|
||||
|
||||
падение ниже 0.9 = системная проблема SLA
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.4 ENGINEERING PERFORMANCE INDEX
|
||||
|
||||
Смысл:
|
||||
|
||||
качество работы инженеров
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
EPI =
|
||||
(0.3 × SLA compliance engineers) +
|
||||
(0.25 × diagnosis accuracy) +
|
||||
(0.2 × reopen rate inverse) +
|
||||
(0.15 × response time score) +
|
||||
(0.1 × documentation quality)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Важный момент:
|
||||
|
||||
это единственный KPI, который напрямую влияет на:
|
||||
|
||||
> удержание клиентов
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риски:
|
||||
|
||||
EPI < 0.75 → будущие потери клиентов через 1–2 месяца
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.5 INCIDENT STABILITY INDEX
|
||||
|
||||
Смысл:
|
||||
|
||||
насколько система “ломается”
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
ISI = 1 - (incidents / objects × severity_weight)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Где severity_weight:
|
||||
|
||||
критический = 1.0
|
||||
|
||||
средний = 0.5
|
||||
|
||||
низкий = 0.2
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риски:
|
||||
|
||||
рост ISI вниз = деградация инфраструктуры
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.6 SALES FLOW INDEX
|
||||
|
||||
Смысл:
|
||||
|
||||
здоровье входящего потока денег
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
SFI = (audits → SLA conversion rate) × lead quality
|
||||
|
||||
|
||||
---
|
||||
|
||||
Пример:
|
||||
|
||||
конверсия 0.35
|
||||
|
||||
качество лидов 0.8
|
||||
|
||||
|
||||
→ SFI = 0.28
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риски:
|
||||
|
||||
SFI < 0.25 → нет роста MRR
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3.7 OPERATIONAL EFFICIENCY INDEX
|
||||
|
||||
Смысл:
|
||||
|
||||
насколько эффективно работает компания внутри
|
||||
|
||||
|
||||
---
|
||||
|
||||
Формула:
|
||||
|
||||
OEI = revenue / (engineer_hours × cost)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Риски:
|
||||
|
||||
падение → перегруз команды
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ИНТЕРПРЕТАЦИЯ SHS
|
||||
|
||||
|
||||
---
|
||||
|
||||
SHS Состояние системы
|
||||
|
||||
85–100 масштабируемый рост
|
||||
70–85 стабильная работа
|
||||
50–70 скрытые проблемы
|
||||
<50 системный кризис
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
5. ГЛАВНАЯ СИЛА SHS — НЕ ЧИСЛО, А ДИНАМИКА
|
||||
|
||||
|
||||
---
|
||||
|
||||
ВАЖНО:
|
||||
|
||||
Ты смотришь не на значение, а на:
|
||||
|
||||
Δ SHS (изменение)
|
||||
|
||||
ΔSHS = SHS_today - SHS_last_week
|
||||
|
||||
|
||||
---
|
||||
|
||||
Критично:
|
||||
|
||||
падение > 5 пунктов за неделю → тревога
|
||||
|
||||
падение > 10 → кризис
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
6. СИСТЕМА АВТОМАТИЧЕСКИХ СИГНАЛОВ
|
||||
|
||||
|
||||
---
|
||||
|
||||
GREEN ZONE:
|
||||
|
||||
SHS > 80
|
||||
|
||||
нет действий
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
YELLOW ZONE:
|
||||
|
||||
SHS 65–80
|
||||
|
||||
|
||||
👉 действия:
|
||||
|
||||
проверить инженеров
|
||||
|
||||
проверить SLA просадки
|
||||
|
||||
проверить загрузку
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
RED ZONE:
|
||||
|
||||
SHS < 65
|
||||
|
||||
|
||||
👉 действия:
|
||||
|
||||
аудит клиентов
|
||||
|
||||
пересмотр инженеров
|
||||
|
||||
срочный анализ SLA нарушений
|
||||
|
||||
заморозка новых продаж
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
7. КОГДА ПОЯВЛЯЮТСЯ РИСКИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
РИСК №1 — падение EPI
|
||||
|
||||
Причина:
|
||||
|
||||
инженеры начали “делать быстро, но плохо”
|
||||
|
||||
Симптом:
|
||||
|
||||
рост повторных заявок
|
||||
|
||||
жалобы клиентов через 2–3 недели
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
РИСК №2 — рост INCIDENT RATE
|
||||
|
||||
Причина:
|
||||
|
||||
старое оборудование
|
||||
|
||||
плохая эксплуатация
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
РИСК №3 — падение RETENTION
|
||||
|
||||
Самый опасный
|
||||
|
||||
👉 означает:
|
||||
|
||||
> клиент уже не верит системе
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
РИСК №4 — нестабильный SFI
|
||||
|
||||
Причина:
|
||||
|
||||
слабый маркетинг
|
||||
|
||||
неправильный аудит
|
||||
|
||||
некачественные лиды
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
РИСК №5 — падение OEI
|
||||
|
||||
Причина:
|
||||
|
||||
перегруз инженеров
|
||||
|
||||
хаотичные выезды
|
||||
|
||||
нет стандартизации
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
8. КАК ИСПРАВЛЯТЬ РИСКИ
|
||||
|
||||
|
||||
---
|
||||
|
||||
ЕСЛИ ПАДАЕТ SHS:
|
||||
|
||||
|
||||
---
|
||||
|
||||
ШАГ 1 — локализация
|
||||
|
||||
разбить SHS на компоненты:
|
||||
|
||||
где падение?
|
||||
|
||||
SLA?
|
||||
|
||||
инженеры?
|
||||
|
||||
продажи?
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ШАГ 2 — точечное вмешательство
|
||||
|
||||
проблема действие
|
||||
|
||||
EPI падает обучение инженеров + чек-листы
|
||||
retention падает аудит объектов
|
||||
SLA падает перераспределение нагрузки
|
||||
SFI падает фильтрация лидов
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ШАГ 3 — стабилизация
|
||||
|
||||
ограничить новые продажи
|
||||
|
||||
усилить контроль SLA
|
||||
|
||||
снизить нагрузку
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
9. ВАЖНОЕ ПРАВИЛО СИСТЕМЫ
|
||||
|
||||
|
||||
---
|
||||
|
||||
> нельзя “растить бизнес”, если SHS падает
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
10. ВИЗУАЛИЗАЦИЯ SHS (CEO DASHBOARD)
|
||||
|
||||
|
||||
---
|
||||
|
||||
1. SHS LINE
|
||||
|
||||
📈 динамика здоровья компании
|
||||
|
||||
|
||||
---
|
||||
|
||||
2. STACKED INDEX CHART
|
||||
|
||||
SLA
|
||||
|
||||
Revenue
|
||||
|
||||
Engineers
|
||||
|
||||
Retention
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
3. ALERT MAP
|
||||
|
||||
🟥 проблемные зоны
|
||||
|
||||
|
||||
---
|
||||
|
||||
4. ENGINEER IMPACT MAP
|
||||
|
||||
📍 какой инженер влияет на SHS
|
||||
|
||||
|
||||
---
|
||||
|
||||
11. ГЛАВНЫЙ СМЫСЛ SHS
|
||||
|
||||
|
||||
---
|
||||
|
||||
Ты больше не управляешь:
|
||||
|
||||
❌ людьми
|
||||
❌ объектами
|
||||
❌ заявками
|
||||
|
||||
|
||||
---
|
||||
|
||||
Ты управляешь:
|
||||
|
||||
системой стабильности инженерного бизнеса
|
||||
|
||||
|
||||
---
|
||||
|
||||
12. ЕСЛИ ДАЛЬШЕ РАЗВИВАТЬ
|
||||
|
||||
Я могу собрать:
|
||||
|
||||
автоматический SHS калькулятор (Excel / 1C / web)
|
||||
|
||||
real-time dashboard
|
||||
|
||||
систему предиктивных предупреждений (падение SHS за 7–14 дней)
|
||||
|
||||
интеграцию SHS в KPI инженеров
|
||||
|
||||
систему автоматического “замедления продаж” при риске
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
ИТОГ
|
||||
|
||||
SHS — это не метрика.
|
||||
|
||||
> это “нервная система” твоей компании, которая заранее показывает, где бизнес начнёт терять деньги или клиентов
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Если хочешь дальше, следующий логичный шаг:
|
||||
|
||||
сделать тебе полноценную цифровую модель AegisOne (дашборд + CRM + SHS + SLA + KPI инженеров в одной системе)
|
||||
@@ -5,10 +5,15 @@ asyncpg==0.30.0
|
||||
alembic==1.14.0
|
||||
python-multipart==0.0.18
|
||||
jinja2==3.1.4
|
||||
passlib[bcrypt]==1.7.4
|
||||
bcrypt>=4.1.0
|
||||
python-dotenv==1.0.1
|
||||
itsdangerous==2.2.0
|
||||
aiohttp==3.11.11
|
||||
markdown==3.7
|
||||
pydantic==2.10.3
|
||||
pydantic-settings==2.7.0
|
||||
|
||||
# Test
|
||||
pytest>=9.0.0
|
||||
httpx>=0.28.0
|
||||
pytest-asyncio>=1.3.0
|
||||
aiosqlite>=0.22.0
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
-- Migration for v1.5.0: Yandex Disk integration + SLA Tickets + MAX notifications
|
||||
|
||||
-- File sync queue
|
||||
CREATE TABLE IF NOT EXISTS file_sync_queue (
|
||||
id SERIAL PRIMARY KEY,
|
||||
local_path VARCHAR(500) NOT NULL,
|
||||
remote_path VARCHAR(500) DEFAULT '',
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id INTEGER NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','uploading','synced','error')),
|
||||
error TEXT DEFAULT '',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_file_sync_status ON file_sync_queue(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_file_sync_entity ON file_sync_queue(entity_type, entity_id);
|
||||
|
||||
-- Bot tickets
|
||||
CREATE TABLE IF NOT EXISTS bot_tickets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ticket_number VARCHAR(20) UNIQUE NOT NULL,
|
||||
user_id INTEGER REFERENCES bot_users(id) ON DELETE SET NULL,
|
||||
object_id INTEGER REFERENCES objects(id) ON DELETE SET NULL,
|
||||
customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
priority VARCHAR(20) DEFAULT 'normal' CHECK (priority IN ('low','normal','high','urgent')),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'open' CHECK (status IN ('open','in_progress','resolved','closed')),
|
||||
created_by VARCHAR(20) DEFAULT 'bot',
|
||||
assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
sla_contract_id INTEGER REFERENCES sla_contracts(id) ON DELETE SET NULL,
|
||||
has_attachment BOOLEAN DEFAULT FALSE,
|
||||
attachment_type VARCHAR(50) DEFAULT '',
|
||||
attachment_path VARCHAR(500) DEFAULT '',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
resolved_at TIMESTAMP NULL,
|
||||
closed_at TIMESTAMP NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_ticket_status ON bot_tickets(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_ticket_user ON bot_tickets(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_ticket_object ON bot_tickets(object_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_ticket_number ON bot_tickets(ticket_number);
|
||||
|
||||
-- Bot ticket messages
|
||||
CREATE TABLE IF NOT EXISTS bot_ticket_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE,
|
||||
direction VARCHAR(3) NOT NULL CHECK (direction IN ('in','out')),
|
||||
text TEXT DEFAULT '',
|
||||
has_attachment BOOLEAN DEFAULT FALSE,
|
||||
attachment_path VARCHAR(500) DEFAULT '',
|
||||
sender VARCHAR(20) DEFAULT 'client',
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_ticket_msg_ticket ON bot_ticket_messages(ticket_id);
|
||||
|
||||
-- Bot ticket status log
|
||||
CREATE TABLE IF NOT EXISTS bot_ticket_statuses (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE,
|
||||
old_status VARCHAR(20) DEFAULT '',
|
||||
new_status VARCHAR(20) NOT NULL,
|
||||
changed_by VARCHAR(20) DEFAULT 'system',
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
comment TEXT DEFAULT ''
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_ticket_status_log ON bot_ticket_statuses(ticket_id);
|
||||
|
||||
-- Add max_user_id to users table
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS max_user_id BIGINT NULL;
|
||||
|
||||
-- Add Yandex Disk settings
|
||||
INSERT INTO bot_settings (key, value, description) VALUES
|
||||
('yandex_disk_token', 'y0__wgBEMHM9zsYiuxBIMi6sswXML_twIQIyCDWBrrNPjQcoQZR6J2x57oBLcg', 'OAuth токен Яндекс Диска'),
|
||||
('yandex_disk_root', '/AegisOne_Service', 'Корневая папка на Яндекс Диске'),
|
||||
('yandex_disk_client_id', '00fd594faf604161b773a53737bb5462', 'Client ID приложения Яндекс'),
|
||||
('yandex_disk_client_secret', '32a46b6dfd3c499e8cced25fed64b426', 'Client Secret приложения Яндекс'),
|
||||
('storage_backend', 'yandex', 'Режим хранения: local/yandex/both'),
|
||||
('max_file_size_mb', '100', 'Максимальный размер файла (MB)'),
|
||||
('public_link_expiry_days', '30', 'Срок публичных ссылок по умолчанию (дни)'),
|
||||
('auto_archive_enabled', 'true', 'Авто-архивация закрытых обращений'),
|
||||
('auto_archive_after_days', '30', 'Через сколько дней архивировать'),
|
||||
('max_group_chat_id', '', 'ID группового чата MAX для уведомлений')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- Add SLA Ticket feature
|
||||
INSERT INTO bot_features (feature_key, name, description, enabled, sort_order) VALUES
|
||||
('sla_ticket', 'Заявка по SLA', 'Подать заявку о неисправности для клиентов с активным SLA', true, 0)
|
||||
ON CONFLICT (feature_key) DO NOTHING;
|
||||
|
||||
-- Add ticket-related response templates
|
||||
INSERT INTO bot_response_templates (template_key, template_text, description, variables) VALUES
|
||||
('sla_ticket_check', 'Проверяю ваш SLA контракт...', 'Проверка SLA перед созданием заявки', '[]'),
|
||||
('sla_ticket_found', 'SLA найден: {object_name}, уровень: {service_level}. Опишите неисправность. Вы можете прикрепить фото или документ.', 'SLA найден, запрос описания', '["object_name","service_level"]'),
|
||||
('sla_ticket_not_found', 'SLA контракт не найден или не активен. Свяжитесь с нами:\n\n📞 {phone_1}\n📞 {phone_2}\n✉️ {support_email}', 'SLA не найден', '["phone_1","phone_2","support_email"]'),
|
||||
('sla_ticket_created', 'Заявка #{ticket_number} создана.\n\nПриоритет: {priority}\nОжидаемое время реакции: {response_time}\n\nИнженер будет назначен в ближайшее время.', 'Подтверждение создания заявки', '["ticket_number","priority","response_time"]'),
|
||||
('ticket_status_update', 'Ваша заявка #{ticket_number} — {status}.\nИнженер: {engineer}', 'Уведомление о смене статуса заявки', '["ticket_number","status","engineer"]'),
|
||||
('ticket_resolved', 'Заявка #{ticket_number} выполнена. Оцените качество:\n\n⭐ 1 — Плохо\n⭐⭐ 2\n⭐⭐⭐ 3\n⭐⭐⭐⭐ 4\n⭐⭐⭐⭐⭐ 5 — Отлично', 'Запрос оценки после выполнения', '["ticket_number"]')
|
||||
ON CONFLICT (template_key) DO NOTHING;
|
||||
@@ -0,0 +1,121 @@
|
||||
-- Migration v1.5.5: New tables + seed data
|
||||
-- Run: psql -U aegisone -d aegisone -f v1.5.5_tables_and_seed.sql
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ===================== New tables =====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bot_notifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
type VARCHAR(50) NOT NULL DEFAULT 'info',
|
||||
recipient_type VARCHAR(20) NOT NULL DEFAULT 'operator',
|
||||
recipient_max_user_id BIGINT NULL,
|
||||
title VARCHAR(255) DEFAULT '',
|
||||
body TEXT DEFAULT '',
|
||||
related_type VARCHAR(50) DEFAULT '',
|
||||
related_id INTEGER NULL,
|
||||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CHECK (recipient_type IN ('operator','level2','client'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bot_notification_subscriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES bot_users(id) ON DELETE CASCADE,
|
||||
notify_on_status_change BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
notify_on_reply BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
notify_on_broadcast BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bot_risk_questions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
question_key VARCHAR(100) UNIQUE NOT NULL,
|
||||
question VARCHAR(500) NOT NULL,
|
||||
weight INTEGER NOT NULL DEFAULT 10,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bot_tickets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ticket_number VARCHAR(20) UNIQUE NOT NULL,
|
||||
user_id INTEGER REFERENCES bot_users(id) ON DELETE SET NULL,
|
||||
object_id INTEGER NULL,
|
||||
customer_id INTEGER NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
priority VARCHAR(20) DEFAULT 'normal',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'open',
|
||||
created_by VARCHAR(20) DEFAULT 'bot',
|
||||
sla_contract_id INTEGER NULL,
|
||||
has_attachment BOOLEAN DEFAULT FALSE,
|
||||
attachment_type VARCHAR(50) DEFAULT '',
|
||||
attachment_path VARCHAR(500) DEFAULT '',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CHECK (priority IN ('low','normal','high','urgent')),
|
||||
CHECK (status IN ('open','in_progress','resolved','closed'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bot_ticket_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE NOT NULL,
|
||||
direction VARCHAR(10) NOT NULL,
|
||||
text TEXT DEFAULT '',
|
||||
sender VARCHAR(50) DEFAULT '',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bot_ticket_statuses (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE NOT NULL,
|
||||
old_status VARCHAR(20) DEFAULT '',
|
||||
new_status VARCHAR(20) NOT NULL,
|
||||
changed_by VARCHAR(50) DEFAULT '',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ===================== Add recipient_ids to broadcasts if missing =====================
|
||||
ALTER TABLE bot_broadcasts ADD COLUMN IF NOT EXISTS recipient_ids JSONB DEFAULT '[]'::jsonb;
|
||||
|
||||
-- ===================== Ensure new columns exist on existing tables =====================
|
||||
ALTER TABLE bot_notifications ADD COLUMN IF NOT EXISTS recipient_max_user_id BIGINT NULL;
|
||||
ALTER TABLE bot_notifications ADD COLUMN IF NOT EXISTS related_type VARCHAR(50) DEFAULT '';
|
||||
ALTER TABLE bot_notifications ADD COLUMN IF NOT EXISTS related_id INTEGER NULL;
|
||||
|
||||
ALTER TABLE bot_notification_subscriptions ADD COLUMN IF NOT EXISTS notify_on_status_change BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE bot_notification_subscriptions ADD COLUMN IF NOT EXISTS notify_on_reply BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE bot_notification_subscriptions ADD COLUMN IF NOT EXISTS notify_on_broadcast BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- Drop old subscribe_types column if it exists (replaced by boolean columns)
|
||||
ALTER TABLE bot_notification_subscriptions DROP COLUMN IF EXISTS subscribe_types;
|
||||
|
||||
-- ===================== Seed: enable all features =====================
|
||||
UPDATE bot_features SET enabled = true WHERE enabled = false;
|
||||
|
||||
-- ===================== Seed: risk_questions =====================
|
||||
INSERT INTO bot_risk_questions (question_key, question, weight, sort_order, is_active) VALUES
|
||||
('no_archive', '📋 Отсутствует архив документации?', 25, 1, true),
|
||||
('no_power_backup', '🔋 Нет резервного питания?', 20, 2, true),
|
||||
('no_regulations', '📜 Нет регламентов обслуживания?', 15, 3, true),
|
||||
('system_failures', '⚠️ Были отказы системы за последний год?', 20, 4, true),
|
||||
('no_documentation', '🗂️ Нет схем и паспортов объекта?', 10, 5, true),
|
||||
('outdated_equipment', '🕰️ Оборудование старше 5 лет?', 5, 6, true),
|
||||
('no_monitoring', '📡 Нет удалённого мониторинга?', 5, 7, true)
|
||||
ON CONFLICT (question_key) DO NOTHING;
|
||||
|
||||
-- ===================== Seed: default role permissions for new menu items =====================
|
||||
INSERT INTO role_menu_permissions (role, menu_key, visible)
|
||||
SELECT * FROM (VALUES
|
||||
('engineer', 'bot_tickets', true),
|
||||
('engineer', 'bot_notifications', true),
|
||||
('engineer', 'bot_risk_questions', true)
|
||||
) AS v(role, menu_key, visible)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM role_menu_permissions r
|
||||
WHERE r.role = v.role AND r.menu_key = v.menu_key
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -133,23 +133,6 @@ ROUTE_PERMISSIONS: dict[tuple[str, str], tuple[str, ...]] = {
|
||||
("POST", "/service/cases/edit"): ("owner",),
|
||||
("POST", "/service/cases/delete"): ("owner",),
|
||||
|
||||
# === BOT SETTINGS (pages) ===
|
||||
("GET", "/service/bot-settings"): ("owner",),
|
||||
("GET", "/service/bot-settings/responses"): ("owner",),
|
||||
("GET", "/service/bot-settings/features"): ("owner",),
|
||||
("GET", "/service/bot-settings/categories"): ("owner",),
|
||||
("GET", "/service/bot-settings/kb"): ("owner",),
|
||||
("GET", "/service/bot-settings/conversations"): ("owner",),
|
||||
("GET", "/service/bot-settings/users"): ("owner",),
|
||||
("GET", "/service/bot-settings/test-runner"): ("owner",),
|
||||
("GET", "/service/bot-settings/analytics"): ("owner",),
|
||||
("GET", "/service/bot-settings/risk-questions"): ("owner",),
|
||||
("GET", "/service/bot-settings/handoffs"): ("owner",),
|
||||
("GET", "/service/bot-settings/notifications"): ("owner",),
|
||||
("GET", "/service/bot-settings/broadcasts"): ("owner",),
|
||||
("GET", "/service/bot-settings/storage"): ("owner",),
|
||||
("GET", "/service/bot-settings/tickets"): ("owner",),
|
||||
|
||||
# === PORTAL / ROLE SETTINGS ===
|
||||
("GET", "/service/portal-settings"): ("owner",),
|
||||
("GET", "/service/role-settings"): ("owner",),
|
||||
@@ -162,57 +145,6 @@ ROUTE_PERMISSIONS: dict[tuple[str, str], tuple[str, ...]] = {
|
||||
("POST", "/service/api/role/switch"): ("owner", "engineer"),
|
||||
("POST", "/service/api/role/reset"): ("authenticated",),
|
||||
|
||||
# === BOT API (proxied) ===
|
||||
("GET", "/service/api/bot/settings"): ("owner",),
|
||||
("POST", "/service/api/bot/settings/save"): ("owner",),
|
||||
("GET", "/service/api/bot/templates"): ("owner",),
|
||||
("POST", "/service/api/bot/templates/save"): ("owner",),
|
||||
("GET", "/service/api/bot/features"): ("owner",),
|
||||
("POST", "/service/api/bot/features/toggle"): ("owner",),
|
||||
("GET", "/service/api/bot/categories"): ("owner",),
|
||||
("POST", "/service/api/bot/categories/create"): ("owner",),
|
||||
("POST", "/service/api/bot/categories/edit"): ("owner",),
|
||||
("POST", "/service/api/bot/categories/delete"): ("owner",),
|
||||
("GET", "/service/api/bot/kb"): ("owner",),
|
||||
("POST", "/service/api/bot/kb/create"): ("owner",),
|
||||
("POST", "/service/api/bot/kb/edit"): ("owner",),
|
||||
("POST", "/service/api/bot/kb/delete"): ("owner",),
|
||||
("GET", "/service/api/bot/conversations"): ("owner",),
|
||||
("GET", "/service/api/bot/users"): ("owner",),
|
||||
("GET", "/service/api/bot/analytics"): ("owner",),
|
||||
("POST", "/service/api/bot/test-run"): ("owner",),
|
||||
("GET", "/service/api/bot/broadcast/recipients"): ("owner",),
|
||||
("GET", "/service/api/bot/broadcast/history"): ("owner",),
|
||||
("POST", "/service/api/bot/broadcast"): ("owner",),
|
||||
|
||||
# === BOT RISK QUESTIONS ===
|
||||
("GET", "/service/api/bot/risk-questions"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/create"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/edit"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/toggle"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/delete"): ("owner",),
|
||||
|
||||
# === BOT STORAGE ===
|
||||
("GET", "/service/api/bot/storage/list"): ("owner",),
|
||||
("GET", "/service/api/bot/storage/search"): ("owner",),
|
||||
("GET", "/service/api/bot/storage/public-link"): ("owner",),
|
||||
("GET", "/service/api/bot/storage/download"): ("owner",),
|
||||
|
||||
# === BOT TICKETS ===
|
||||
("GET", "/service/api/bot/tickets"): ("owner",),
|
||||
("GET", "/service/api/bot/tickets/{ticket_id}"): ("owner",),
|
||||
("POST", "/service/api/bot/tickets/create"): ("owner",),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/message"): ("owner",),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/status"): ("owner",),
|
||||
("GET", "/service/api/bot/engineers"): ("owner",),
|
||||
|
||||
# === BOT HANDOFFS / NOTIFICATIONS (non-standard paths) ===
|
||||
("GET", "/service/bot-settings/handoffs/poll"): ("owner",),
|
||||
("POST", "/service/bot-settings/handoffs/{handoff_id}/take"): ("owner",),
|
||||
("POST", "/service/api/bot/handoffs/{handoff_id}/close"): ("owner",),
|
||||
("GET", "/service/bot-settings/notifications/data"): ("owner",),
|
||||
("POST", "/service/bot-settings/notifications/{notif_id}/read"): ("owner",),
|
||||
|
||||
# === OTHER API (main.py) ===
|
||||
("GET", "/service/api/shs"): ("owner", "engineer", "technician"),
|
||||
("GET", "/service/api/tasks"): ("owner", "engineer", "technician"),
|
||||
@@ -241,12 +173,6 @@ DYNAMIC_ROUTES: set[tuple[str, str]] = {
|
||||
("GET", "/service/documents/{slug}/edit"),
|
||||
("POST", "/service/documents/{slug}/edit"),
|
||||
("GET", "/service/documents/{slug}/download"),
|
||||
("POST", "/service/bot-settings/handoffs/{handoff_id}/take"),
|
||||
("POST", "/service/bot-settings/notifications/{notif_id}/read"),
|
||||
("POST", "/service/api/bot/handoffs/{handoff_id}/close"),
|
||||
("GET", "/service/api/bot/tickets/{ticket_id}"),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/message"),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/status"),
|
||||
}
|
||||
|
||||
# Routes excluded from discover test (internal framework routes)
|
||||
|
||||
@@ -36,15 +36,6 @@ PROTECTED_GET_ROUTES = [
|
||||
"/service/charts",
|
||||
"/service/portal-settings",
|
||||
"/service/role-settings",
|
||||
"/service/bot-settings",
|
||||
"/service/bot-settings/responses",
|
||||
"/service/bot-settings/features",
|
||||
"/service/bot-settings/categories",
|
||||
"/service/bot-settings/kb",
|
||||
"/service/bot-settings/conversations",
|
||||
"/service/bot-settings/users",
|
||||
"/service/bot-settings/test-runner",
|
||||
"/service/bot-settings/analytics",
|
||||
"/service/quick-menu",
|
||||
"/service/documents/",
|
||||
]
|
||||
@@ -56,16 +47,6 @@ API_GET_ROUTES = [
|
||||
"/service/api/quick-menu",
|
||||
"/service/api/role-permissions",
|
||||
"/service/api/charts/data",
|
||||
"/service/api/bot/settings",
|
||||
"/service/api/bot/templates",
|
||||
"/service/api/bot/features",
|
||||
"/service/api/bot/categories",
|
||||
"/service/api/bot/kb",
|
||||
"/service/api/bot/conversations",
|
||||
"/service/api/bot/users",
|
||||
"/service/api/bot/analytics",
|
||||
"/service/api/bot/broadcast/history",
|
||||
"/service/api/bot/broadcast/recipients",
|
||||
]
|
||||
|
||||
|
||||
@@ -151,24 +132,8 @@ class TestEngineerAccess:
|
||||
"/service/charts",
|
||||
"/service/portal-settings",
|
||||
"/service/role-settings",
|
||||
"/service/bot-settings",
|
||||
"/service/bot-settings/responses",
|
||||
"/service/bot-settings/features",
|
||||
"/service/bot-settings/categories",
|
||||
"/service/bot-settings/kb",
|
||||
"/service/bot-settings/users",
|
||||
"/service/bot-settings/test-runner",
|
||||
"/service/bot-settings/analytics",
|
||||
"/service/bot-settings/conversations",
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("path", ENGINEER_ALLOWED)
|
||||
def test_allowed(self, engineer_client, path):
|
||||
resp = engineer_client.get(path, follow_redirects=False)
|
||||
assert resp.status_code in (200, 302, 500), f"GET {path} returned {resp.status_code}"
|
||||
if resp.status_code == 500:
|
||||
pytest.fail(f"GET {path} returned 500 Internal Server Error")
|
||||
|
||||
@pytest.mark.parametrize("path", ENGINEER_FORBIDDEN)
|
||||
def test_forbidden(self, engineer_client, path):
|
||||
resp = engineer_client.get(path, follow_redirects=False)
|
||||
@@ -205,7 +170,6 @@ class TestTechnicianAccess:
|
||||
"/service/charts",
|
||||
"/service/portal-settings",
|
||||
"/service/role-settings",
|
||||
"/service/bot-settings",
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("path", TECHNICIAN_ALLOWED)
|
||||
@@ -239,14 +203,10 @@ class TestSidebarRoutes:
|
||||
"/service/incidents",
|
||||
"/service/checklist",
|
||||
"/service/documents/tech-access",
|
||||
"/service/bot-settings/analytics",
|
||||
"/service/bot-settings/users",
|
||||
"/service/bot-settings/conversations",
|
||||
"/service/blog",
|
||||
"/service/cases",
|
||||
"/service/questionnaire-config",
|
||||
"/service/formulas",
|
||||
"/service/bot-settings",
|
||||
"/service/portal-settings",
|
||||
"/service/role-settings",
|
||||
"/service/ideas",
|
||||
|
||||
@@ -86,42 +86,12 @@ EXPECTED_ROUTES = [
|
||||
("POST", "/service/cases/create"),
|
||||
("POST", "/service/cases/edit"),
|
||||
("POST", "/service/cases/delete"),
|
||||
("GET", "/service/bot-settings"),
|
||||
("GET", "/service/bot-settings/responses"),
|
||||
("GET", "/service/bot-settings/features"),
|
||||
("GET", "/service/bot-settings/categories"),
|
||||
("GET", "/service/bot-settings/kb"),
|
||||
("GET", "/service/bot-settings/conversations"),
|
||||
("GET", "/service/bot-settings/users"),
|
||||
("GET", "/service/bot-settings/test-runner"),
|
||||
("GET", "/service/bot-settings/analytics"),
|
||||
("GET", "/service/portal-settings"),
|
||||
("GET", "/service/role-settings"),
|
||||
("GET", "/service/api/role-permissions"),
|
||||
("POST", "/service/api/role-permissions/save"),
|
||||
("POST", "/service/api/role/switch"),
|
||||
("POST", "/service/api/role/reset"),
|
||||
("GET", "/service/api/bot/settings"),
|
||||
("POST", "/service/api/bot/settings/save"),
|
||||
("GET", "/service/api/bot/templates"),
|
||||
("POST", "/service/api/bot/templates/save"),
|
||||
("GET", "/service/api/bot/features"),
|
||||
("POST", "/service/api/bot/features/toggle"),
|
||||
("GET", "/service/api/bot/categories"),
|
||||
("POST", "/service/api/bot/categories/create"),
|
||||
("POST", "/service/api/bot/categories/edit"),
|
||||
("POST", "/service/api/bot/categories/delete"),
|
||||
("GET", "/service/api/bot/kb"),
|
||||
("POST", "/service/api/bot/kb/create"),
|
||||
("POST", "/service/api/bot/kb/edit"),
|
||||
("POST", "/service/api/bot/kb/delete"),
|
||||
("GET", "/service/api/bot/conversations"),
|
||||
("GET", "/service/api/bot/users"),
|
||||
("GET", "/service/api/bot/analytics"),
|
||||
("POST", "/service/api/bot/test-run"),
|
||||
("GET", "/service/api/bot/broadcast/recipients"),
|
||||
("GET", "/service/api/bot/broadcast/history"),
|
||||
("POST", "/service/api/bot/broadcast"),
|
||||
]
|
||||
|
||||
|
||||
@@ -159,14 +129,10 @@ class TestRoutesDefined:
|
||||
"/service/incidents",
|
||||
"/service/checklist",
|
||||
"/service/documents/tech-access",
|
||||
"/service/bot-settings/analytics",
|
||||
"/service/bot-settings/users",
|
||||
"/service/bot-settings/conversations",
|
||||
"/service/blog",
|
||||
"/service/cases",
|
||||
"/service/questionnaire-config",
|
||||
"/service/formulas",
|
||||
"/service/bot-settings",
|
||||
"/service/portal-settings",
|
||||
"/service/role-settings",
|
||||
"/service/ideas",
|
||||
|
||||
@@ -36,15 +36,6 @@ ROUTES_TO_CHECK = [
|
||||
"/service/formulas",
|
||||
"/service/coefficients",
|
||||
"/service/coefficients/test",
|
||||
"/service/bot-settings",
|
||||
"/service/bot-settings/responses",
|
||||
"/service/bot-settings/features",
|
||||
"/service/bot-settings/categories",
|
||||
"/service/bot-settings/kb",
|
||||
"/service/bot-settings/conversations",
|
||||
"/service/bot-settings/users",
|
||||
"/service/bot-settings/test-runner",
|
||||
"/service/bot-settings/analytics",
|
||||
"/service/portal-settings",
|
||||
"/service/role-settings",
|
||||
"/service/quick-menu",
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.5.4
|
||||
1.7.0
|
||||
|
||||
Reference in New Issue
Block a user