init infrait

This commit is contained in:
2026-02-12 18:47:12 +03:00
commit 7a6ecf6281
18 changed files with 1389 additions and 0 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
*.db-shm
*.db-wal
.git
.gitignore
Dockerfile*
docker-compose*.yml

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
# Copy to .env and set your own value
SECRET_KEY=replace-with-a-long-random-string

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/data /app/static/img
EXPOSE 4545
CMD ["gunicorn", "-c", "gunicorn_conf.py", "app:app"]

Binary file not shown.

255
app.py Normal file
View File

@@ -0,0 +1,255 @@
from __future__ import annotations
import os
import sqlite3
from datetime import datetime
from functools import wraps
from pathlib import Path
from urllib import parse, request as urllib_request
from flask import Flask, redirect, render_template, request, session, url_for
from werkzeug.security import check_password_hash
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
DB_PATH = DATA_DIR / "infra.db"
DEFAULT_SETTINGS = {
"company_name": "InfraIT",
"phone_display": "+7 987 297-06-66",
"phone_link": "+79872970666",
"email": "maks@infrait.ru",
"site_url": "https://infrait.ru/",
"yandex_verification": "PASTE_YOUR_YANDEX_VERIFICATION_TOKEN",
"yandex_metrika_id": "",
"telegram_bot_token": "",
"telegram_chat_id": "",
"geo_primary": "Казань и Татарстан — выезд в день запроса",
"geo_secondary": "Россия — удалённая поддержка",
}
app = Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY", "change-this-secret-key")
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
# Fixed admin password hash (plain password is never stored in code).
ADMIN_PASSWORD_HASH = (
"scrypt:32768:8:1$Ac0t7TD7bUhYLg04$25779398c765417771b888aa15d23dd72ee40bea4e48d0cd"
"7da9e8e386628a099b1f1e75019059be76c73264deb888959c236f6b776d12f4847e6762d5c76f0f"
)
def get_db() -> sqlite3.Connection:
DATA_DIR.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA busy_timeout=30000;")
conn.execute("PRAGMA synchronous=NORMAL;")
conn.row_factory = sqlite3.Row
return conn
def init_db() -> None:
with get_db() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
name TEXT NOT NULL,
company TEXT NOT NULL,
phone TEXT NOT NULL,
email TEXT,
city TEXT,
computers TEXT NOT NULL,
message TEXT
)
"""
)
for key, value in DEFAULT_SETTINGS.items():
conn.execute(
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",
(key, value),
)
conn.commit()
def get_settings() -> dict[str, str]:
settings = DEFAULT_SETTINGS.copy()
with get_db() as conn:
rows = conn.execute("SELECT key, value FROM settings").fetchall()
for row in rows:
settings[row["key"]] = row["value"]
return settings
def update_settings(new_values: dict[str, str]) -> None:
with get_db() as conn:
for key, value in new_values.items():
conn.execute(
"""
INSERT INTO settings (key, value)
VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value
""",
(key, value),
)
conn.commit()
def send_telegram_lead(form_data: dict[str, str], settings: dict[str, str]) -> None:
token = settings.get("telegram_bot_token", "").strip()
chat_id = settings.get("telegram_chat_id", "").strip()
if not token or not chat_id:
return
message = (
"Новая заявка InfraIT\n"
f"Имя: {form_data['name']}\n"
f"Компания: {form_data['company']}\n"
f"Телефон: {form_data['phone']}\n"
f"Email: {form_data['email'] or '-'}\n"
f"Город: {form_data['city'] or '-'}\n"
f"ПК: {form_data['computers']}\n"
f"Комментарий: {form_data['message'] or '-'}"
)
payload = parse.urlencode({"chat_id": chat_id, "text": message}).encode("utf-8")
req = urllib_request.Request(
f"https://api.telegram.org/bot{token}/sendMessage",
data=payload,
method="POST",
)
try:
urllib_request.urlopen(req, timeout=8).read()
except Exception:
# Ошибка телеграм-уведомления не должна ломать отправку формы.
return
def save_lead(form_data: dict[str, str], settings: dict[str, str]) -> None:
with get_db() as conn:
conn.execute(
"""
INSERT INTO leads (
created_at, name, company, phone, email, city, computers, message
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
datetime.now().isoformat(timespec="seconds"),
form_data["name"],
form_data["company"],
form_data["phone"],
form_data["email"],
form_data["city"],
form_data["computers"],
form_data["message"],
),
)
conn.commit()
send_telegram_lead(form_data, settings)
def admin_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not session.get("admin_logged_in"):
return redirect(url_for("admin_login"))
return view(*args, **kwargs)
return wrapped
def verify_admin_password(password: str) -> bool:
return check_password_hash(ADMIN_PASSWORD_HASH, password)
init_db()
@app.route("/", methods=["GET", "POST"])
def index():
success = request.args.get("success") == "1"
error = None
settings = get_settings()
if request.method == "POST":
form_data = {
"name": request.form.get("name", "").strip(),
"company": request.form.get("company", "").strip(),
"phone": request.form.get("phone", "").strip(),
"email": request.form.get("email", "").strip(),
"city": request.form.get("city", "").strip(),
"computers": request.form.get("computers", "").strip(),
"message": request.form.get("message", "").strip(),
}
required_fields = ["name", "phone", "company", "computers"]
if any(not form_data[field] for field in required_fields):
error = "Заполните обязательные поля: имя, компания, телефон и количество компьютеров."
else:
save_lead(form_data, settings)
return redirect(url_for("index", success=1) + "#contact")
return render_template("index.html", success=success, error=error, settings=settings)
@app.route("/admin/login", methods=["GET", "POST"])
def admin_login():
error = None
if request.method == "POST":
password = request.form.get("password", "")
if verify_admin_password(password):
session["admin_logged_in"] = True
return redirect(url_for("admin_settings"))
error = "Неверный пароль."
return render_template("admin_login.html", error=error)
@app.route("/admin/logout")
def admin_logout():
session.pop("admin_logged_in", None)
return redirect(url_for("admin_login"))
@app.route("/admin/settings", methods=["GET", "POST"])
@admin_required
def admin_settings():
success = request.args.get("saved") == "1"
settings = get_settings()
if request.method == "POST":
updates: dict[str, str] = {}
for field in DEFAULT_SETTINGS:
updates[field] = request.form.get(field, "").strip()
update_settings(updates)
return redirect(url_for("admin_settings", saved=1))
return render_template("admin_settings.html", settings=settings, success=success)
@app.route("/health")
def health():
try:
with get_db() as conn:
conn.execute("SELECT 1")
return {"status": "ok"}, 200
except Exception:
return {"status": "error"}, 503
if __name__ == "__main__":
init_db()
app.run(debug=True)

BIN
data/infra.db Normal file

Binary file not shown.

2
data/leads.csv Normal file
View File

@@ -0,0 +1,2 @@
created_at,name,company,phone,email,city,computers,message
2026-02-12T16:33:08,Иван,ООО Тест,123,,,10,
1 created_at name company phone email city computers message
2 2026-02-12T16:33:08 Иван ООО Тест 123 10

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
version: "3.9"
services:
web:
build: .
container_name: infrait-web
restart: unless-stopped
ports:
- "4545:4545"
volumes:
- ./data:/app/data
- ./static/img:/app/static/img
environment:
- SECRET_KEY=${SECRET_KEY:-change-this-in-prod}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4545/health', timeout=3)"]
interval: 20s
timeout: 5s
retries: 5
start_period: 20s

14
gunicorn_conf.py Normal file
View File

@@ -0,0 +1,14 @@
import multiprocessing
bind = "0.0.0.0:4545"
workers = max(4, multiprocessing.cpu_count() * 2)
threads = 4
timeout = 60
graceful_timeout = 30
keepalive = 5
worker_tmp_dir = "/dev/shm"
max_requests = 1000
max_requests_jitter = 100
accesslog = "-"
errorlog = "-"
loglevel = "info"

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
Flask==3.1.0
gunicorn==23.0.0

559
static/css/styles.css Normal file
View File

@@ -0,0 +1,559 @@
:root {
--bg: #f2f5f8;
--surface: #ffffff;
--surface-2: #e9eef3;
--text: #112031;
--muted: #5a6b7d;
--brand: #0f8c7a;
--brand-dark: #0b695c;
--accent: #f59f00;
--border: #d6e0ea;
--shadow: 0 12px 30px rgba(17, 32, 49, 0.08);
--radius: 18px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: "Manrope", sans-serif;
color: var(--text);
line-height: 1.5;
background:
radial-gradient(circle at 15% -10%, #d4fff6 0, transparent 34%),
radial-gradient(circle at 90% 0%, #ffefcc 0, transparent 28%),
var(--bg);
}
.container {
width: min(1140px, 92vw);
margin: 0 auto;
}
.hero {
padding: 24px 0 40px;
}
.topbar {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 18px;
margin-bottom: 36px;
}
.brand {
display: inline-flex;
align-items: center;
text-decoration: none;
margin-right: auto;
}
.brand img {
display: block;
width: clamp(273px, 29vw, 399px);
height: auto;
object-fit: contain;
transform: translateX(-18px);
}
.brand-fallback {
display: none;
font-family: "Montserrat", sans-serif;
font-size: 1.35rem;
font-weight: 800;
letter-spacing: 0.4px;
color: var(--text);
}
.nav {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: flex-end;
}
.nav a {
font-weight: 600;
opacity: 0.88;
}
.nav a,
.phone,
.footer a {
text-decoration: none;
color: var(--text);
}
.phone {
font-weight: 700;
}
.hero-grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 24px;
align-items: start;
}
.badge {
display: inline-block;
background: #d6fff6;
color: var(--brand-dark);
border-radius: 999px;
padding: 7px 14px;
font-weight: 700;
font-size: 0.9rem;
}
h1,
h2,
h3 {
font-family: "Montserrat", sans-serif;
margin-top: 0;
line-height: 1.2;
}
h1 {
margin: 16px 0;
font-size: clamp(1.8rem, 3vw, 2.8rem);
}
h2 {
margin-bottom: 10px;
font-size: clamp(1.5rem, 2.2vw, 2.2rem);
}
.lead,
.section-subtitle {
color: var(--muted);
font-size: 1.03rem;
}
.cta-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 22px 0;
}
.btn {
border: 0;
border-radius: 12px;
padding: 12px 18px;
font-weight: 700;
text-decoration: none;
display: inline-block;
transition: 0.2s ease;
}
.btn-primary {
background: var(--brand);
color: #fff;
}
.btn-primary:hover {
background: var(--brand-dark);
}
.btn-secondary {
background: var(--surface-2);
color: var(--text);
}
.btn-secondary:hover {
background: #dce4ed;
}
.geo-note {
margin-top: 8px;
padding: 13px 15px;
border-left: 4px solid var(--accent);
background: #fff9ec;
border-radius: 0 10px 10px 0;
}
.hero-card,
.card,
.price-card,
.panel,
.lead-form,
.faq-list details {
background: var(--surface);
border: 1px solid var(--border);
box-shadow: var(--shadow);
border-radius: var(--radius);
}
.hero-card {
padding: 20px;
}
.hero-card h3 {
margin-bottom: 10px;
}
.hero-card ul,
.card ul,
.price-card ul,
.panel ul,
.contacts,
.panel ol {
margin: 0;
padding-left: 20px;
}
.section {
padding: 46px 0;
}
.cards {
margin-top: 22px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.cards.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.card {
padding: 18px;
}
.card h3 {
margin-bottom: 8px;
}
.highlight,
.muted {
background: linear-gradient(180deg, rgba(15, 140, 122, 0.06), rgba(15, 140, 122, 0));
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.price-card {
padding: 20px;
}
.price-card.featured {
border: 2px solid var(--brand);
transform: translateY(-6px);
}
.chip {
display: inline-block;
background: var(--brand);
color: #fff;
border-radius: 999px;
padding: 4px 10px;
font-size: 0.83rem;
margin-bottom: 8px;
}
.price {
font-size: 1.35rem;
font-weight: 800;
margin: 8px 0;
}
.for,
.pricing-note {
color: var(--muted);
}
.two-col {
display: grid;
gap: 16px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.panel {
padding: 20px;
}
.benefits-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.benefits-grid article {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px;
}
.seo p {
color: #25384d;
}
.faq-list {
display: grid;
gap: 10px;
}
.faq-list details {
padding: 14px;
}
.faq-list summary {
cursor: pointer;
font-weight: 700;
}
.contact {
background: linear-gradient(175deg, #e8fff8 0%, #eff5ff 100%);
}
.contacts {
list-style: none;
padding: 0;
}
.contacts li {
margin-bottom: 8px;
}
.lead-form {
padding: 18px;
display: grid;
gap: 10px;
}
.lead-form label {
display: grid;
gap: 5px;
font-weight: 600;
}
input,
textarea {
width: 100%;
border: 1px solid #bfd0df;
border-radius: 10px;
padding: 10px;
font: inherit;
}
input:focus,
textarea:focus {
outline: 2px solid rgba(15, 140, 122, 0.28);
border-color: var(--brand);
}
.form-success,
.form-error {
margin: 0;
border-radius: 9px;
padding: 10px;
font-weight: 700;
}
.form-success {
background: #dcffee;
color: #0f684f;
}
.form-error {
background: #ffe2e2;
color: #ab2a2a;
}
.footer {
border-top: 1px solid var(--border);
background: #f8fbff;
}
.footer-inner {
padding: 18px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.footer-logo {
display: inline-flex;
margin-bottom: 8px;
}
.footer-logo img {
width: clamp(231px, 25vw, 336px);
height: auto;
object-fit: contain;
transform: translateX(-18px);
}
.mobile-sticky-cta {
display: none;
}
@media (max-width: 1040px) {
.hero-grid,
.pricing-grid,
.cards,
.benefits-grid,
.two-col,
.cards.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.benefits-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
body {
background:
radial-gradient(circle at 12% -8%, #d4fff6 0, transparent 45%),
radial-gradient(circle at 88% 0%, #ffe7c2 0, transparent 38%),
var(--bg);
}
.container {
width: min(1140px, 94vw);
}
.hero {
padding: 16px 0 24px;
}
.topbar {
flex-direction: column;
align-items: start;
gap: 12px;
margin-bottom: 20px;
}
.brand img {
width: clamp(231px, 62vw, 336px);
transform: translateX(-12px);
}
.nav {
flex-wrap: nowrap;
overflow-x: auto;
width: 100%;
padding-bottom: 4px;
scrollbar-width: thin;
}
.nav a {
white-space: nowrap;
background: #ffffffb3;
border: 1px solid var(--border);
border-radius: 999px;
padding: 7px 12px;
font-size: 0.92rem;
}
.phone {
background: var(--surface);
border: 1px solid var(--border);
padding: 9px 12px;
border-radius: 10px;
}
.hero-grid,
.pricing-grid,
.cards,
.cards.two,
.two-col,
.benefits-grid {
grid-template-columns: 1fr;
}
.price-card.featured {
transform: none;
}
h1 {
font-size: clamp(1.6rem, 8vw, 2.1rem);
margin: 12px 0;
}
h2 {
font-size: clamp(1.35rem, 7vw, 1.8rem);
}
.lead {
font-size: 0.98rem;
}
.cta-row {
gap: 10px;
}
.btn {
width: 100%;
text-align: center;
padding: 13px 16px;
}
.hero-card,
.card,
.price-card,
.panel,
.lead-form,
.faq-list details {
border-radius: 14px;
box-shadow: 0 8px 22px rgba(17, 32, 49, 0.08);
}
.section {
padding: 32px 0;
}
.cards,
.pricing-grid,
.faq-list {
gap: 12px;
}
.lead-form {
gap: 9px;
padding: 14px;
}
.footer-inner {
flex-direction: column;
align-items: start;
padding-bottom: 84px;
}
.footer-logo img {
width: clamp(210px, 56vw, 315px);
transform: translateX(-10px);
}
.mobile-sticky-cta {
position: fixed;
left: 12px;
right: 12px;
bottom: 12px;
display: block;
text-align: center;
text-decoration: none;
background: linear-gradient(90deg, var(--brand), #22a88f);
color: #fff;
font-weight: 800;
padding: 14px 16px;
border-radius: 12px;
box-shadow: 0 10px 28px rgba(11, 105, 92, 0.35);
z-index: 999;
}
}

4
static/img/README.txt Normal file
View File

@@ -0,0 +1,4 @@
Положите файл логотипа сюда:
static/img/infrait-logo.png
Текущий шаблон уже подключен к этому пути.

BIN
static/img/infrait-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

23
static/js/main.js Normal file
View File

@@ -0,0 +1,23 @@
(() => {
const revealItems = document.querySelectorAll('.card, .price-card, .panel, .benefits-grid article, .faq-list details');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.08 }
);
revealItems.forEach((item, index) => {
item.style.opacity = '0';
item.style.transform = 'translateY(18px)';
item.style.transition = `opacity .45s ease ${index * 0.03}s, transform .45s ease ${index * 0.03}s`;
observer.observe(item);
});
})();

View File

@@ -0,0 +1,27 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Вход в админку | InfraIT</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Montserrat:wght@600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body>
<main class="section">
<div class="container" style="max-width:560px;">
<h1>Админка InfraIT</h1>
<p class="section-subtitle">Введите пароль администратора.</p>
<form method="post" class="lead-form">
{% if error %}
<p class="form-error">{{ error }}</p>
{% endif %}
<label>Пароль<input type="password" name="password" required></label>
<button type="submit" class="btn btn-primary">Войти</button>
</form>
</div>
</main>
</body>
</html>

View File

@@ -0,0 +1,77 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Настройки сайта | InfraIT</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Montserrat:wght@600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body>
<main class="section">
<div class="container" style="max-width:900px;">
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap;">
<div>
<h1>Личный кабинет администратора</h1>
<p class="section-subtitle">Редактирование контактов, SEO и интеграций.</p>
</div>
<a class="btn btn-secondary" href="{{ url_for('admin_logout') }}">Выйти</a>
</div>
<form method="post" class="lead-form" style="margin-top:14px;">
{% if success %}
<p class="form-success">Настройки сохранены.</p>
{% endif %}
<label>Название компании
<input type="text" name="company_name" value="{{ settings.company_name }}" required>
</label>
<label>Телефон (как отображать)
<input type="text" name="phone_display" value="{{ settings.phone_display }}" required>
</label>
<label>Телефон (для ссылки tel:)
<input type="text" name="phone_link" value="{{ settings.phone_link }}" required>
</label>
<label>Email
<input type="email" name="email" value="{{ settings.email }}" required>
</label>
<label>URL сайта (canonical)
<input type="url" name="site_url" value="{{ settings.site_url }}" required>
</label>
<label>Yandex Verification Token
<input type="text" name="yandex_verification" value="{{ settings.yandex_verification }}">
</label>
<label>ID Яндекс.Метрики
<input type="text" name="yandex_metrika_id" value="{{ settings.yandex_metrika_id }}" placeholder="12345678">
</label>
<label>Telegram Bot Token
<input type="text" name="telegram_bot_token" value="{{ settings.telegram_bot_token }}" placeholder="123456:ABC...">
</label>
<label>Telegram Chat ID
<input type="text" name="telegram_chat_id" value="{{ settings.telegram_chat_id }}" placeholder="-100...">
</label>
<label>География выездов
<input type="text" name="geo_primary" value="{{ settings.geo_primary }}">
</label>
<label>География удаленной работы
<input type="text" name="geo_secondary" value="{{ settings.geo_secondary }}">
</label>
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
</form>
</div>
</main>
</body>
</html>

37
templates/base.html Normal file
View File

@@ -0,0 +1,37 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ settings.company_name }} — IT-аутсорсинг для бизнеса{% endblock %}</title>
<meta name="description" content="InfraIT: комплексное IT-обслуживание бизнеса в Татарстане и Казани с выездом в день запроса. По России — удаленная поддержка. Фиксированная стоимость, быстрое реагирование, поддержка серверов и сетей.">
<meta name="keywords" content="IT аутсорсинг Казань, IT обслуживание Татарстан, обслуживание компьютеров организаций, поддержка серверов, администрирование сетей, IT поддержка бизнеса">
<link rel="canonical" href="{{ settings.site_url }}">
<meta property="og:title" content="InfraIT — комплексное IT-обслуживание бизнеса">
<meta property="og:description" content="Компьютеры, серверы и сети для бизнеса без простоев. Татарстан и Казань с выездом, Россия удаленно.">
<meta property="og:type" content="website">
<meta property="og:locale" content="ru_RU">
<meta name="yandex-verification" content="{{ settings.yandex_verification }}">
{% if settings.yandex_metrika_id %}
<script>
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) { if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym({{ settings.yandex_metrika_id }}, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true });
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/{{ settings.yandex_metrika_id }}" style="position:absolute; left:-9999px;" alt=""></div></noscript>
{% endif %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Montserrat:wght@600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
{% block head_extra %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

339
templates/index.html Normal file
View File

@@ -0,0 +1,339 @@
{% extends "base.html" %}
{% block title %}{{ settings.company_name }} — IT-обслуживание бизнеса в Татарстане и по России{% endblock %}
{% block head_extra %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "{{ settings.company_name }}",
"url": "{{ settings.site_url }}",
"email": "{{ settings.email }}",
"telephone": "{{ settings.phone_link }}",
"areaServed": ["Татарстан", "Казань", "Россия"],
"address": {
"@type": "PostalAddress",
"addressCountry": "RU",
"addressRegion": "Республика Татарстан",
"addressLocality": "Казань"
},
"serviceType": "Комплексное IT-обслуживание бизнеса"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Чем аутсорсинг выгоднее штатного системного администратора?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Вы получаете команду специалистов по цене ниже одного штатного сотрудника, фиксированную стоимость и подмену на время отпусков и больничных."
}
},
{
"@type": "Question",
"name": "Как быстро вы реагируете на заявки?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Большинство обращений берём в работу в течение 1530 минут. Для критичных инцидентов действует приоритетная схема в зависимости от тарифа."
}
},
{
"@type": "Question",
"name": "Вы работаете только в Казани?",
"acceptedAnswer": {
"@type": "Answer",
"text": "В Казани и по Татарстану выезжаем в день запроса. По России оказываем удаленную IT-поддержку и администрирование."
}
}
]
}
</script>
{% endblock %}
{% block content %}
<header class="hero" id="top">
<div class="container">
<div class="topbar">
<a class="brand" href="#top" aria-label="{{ settings.company_name }}">
<img src="{{ url_for('static', filename='img/infrait-logo.png') }}" alt="{{ settings.company_name }} logo" onerror="this.style.display='none'; this.nextElementSibling.style.display='inline-flex';">
<span class="brand-fallback">{{ settings.company_name }}</span>
</a>
<nav class="nav">
<a href="#services">Услуги</a>
<a href="#pricing">Тарифы</a>
<a href="#process">Как работаем</a>
<a href="#faq">FAQ</a>
<a href="#contact">Контакты</a>
</nav>
<a href="tel:{{ settings.phone_link }}" class="phone">{{ settings.phone_display }}</a>
</div>
<div class="hero-grid">
<div>
<span class="badge">IT-аутсорсинг для бизнеса 5-100 ПК</span>
<h1>Комплексное IT-обслуживание компьютеров, серверов и сетей для стабильной работы бизнеса</h1>
<p class="lead">Берём на себя всю IT-часть: от поддержки пользователей до серверов и резервного копирования. Дополнительно обеспечиваем поставку лицензионного ПО и мягкую миграцию на отечественные решения без остановки работы.</p>
<div class="cta-row">
<a href="#contact" class="btn btn-primary">Рассчитать стоимость</a>
<a href="#contact" class="btn btn-secondary">Получить консультацию</a>
</div>
<div class="geo-note">
<strong>География:</strong> {{ settings.geo_primary }}. {{ settings.geo_secondary }}.
</div>
</div>
<aside class="hero-card">
<h3>Почему с InfraIT проще</h3>
<ul>
<li>Аутсорсинг дешевле штатного IT-специалиста</li>
<li>Фиксированная ежемесячная стоимость</li>
<li>Один подрядчик за всю IT-инфраструктуру</li>
<li>Быстрое реагирование на инциденты</li>
<li>Поставка ПО и переход на российские аналоги под ключ</li>
<li>Поддержка роста и масштабирования</li>
</ul>
</aside>
</div>
</div>
</header>
<main>
<section class="section" id="services">
<div class="container">
<h2>Услуги IT-обслуживания</h2>
<p class="section-subtitle">Мы берём на себя всю IT-часть, чтобы бизнес работал без простоев.</p>
<div class="cards">
<article class="card"><h3>Рабочие станции Windows/macOS</h3><p>Настройка, обслуживание, обновления, контроль лицензий и производительности.</p></article>
<article class="card"><h3>Поддержка пользователей</h3><p>Удалённая помощь и выезды по Татарстану, чтобы сотрудники не теряли рабочее время.</p></article>
<article class="card"><h3>Серверы и сервисы</h3><p>Поддержка AD, файловых и прикладных серверов, контроль стабильности и доступа.</p></article>
<article class="card"><h3>Сети и Wi-Fi</h3><p>Администрирование сетевой инфраструктуры, сегментация, защита и оптимизация.</p></article>
<article class="card"><h3>VPN и удалённая работа</h3><p>Безопасный доступ сотрудников к корпоративным ресурсам из любой точки.</p></article>
<article class="card"><h3>Резервное копирование</h3><p>Настройка бэкапов и регулярная проверка восстановления данных.</p></article>
<article class="card"><h3>Мониторинг инфраструктуры</h3><p>Постоянный контроль серверов, сетей и критичных узлов с превентивным реагированием.</p></article>
<article class="card"><h3>Модернизация IT-систем</h3><p>Плановые обновления оборудования и ПО без остановки бизнес-процессов.</p></article>
<article class="card"><h3>IT-консалтинг и планирование</h3><p>Понятный план развития инфраструктуры под задачи и бюджет компании.</p></article>
<article class="card"><h3>Поставка лицензионного ПО</h3><p>Подбираем, закупаем и внедряем софт для бизнеса: ОС, офисные пакеты, безопасность, серверные лицензии.</p></article>
<article class="card"><h3>Миграция на отечественные решения</h3><p>Переход на российское ПО поэтапно: аудит, пилот, перенос данных, обучение сотрудников и сопровождение.</p></article>
</div>
</div>
</section>
<section class="section highlight" id="pricing">
<div class="container">
<h2>Тарифы обслуживания</h2>
<p class="section-subtitle">Стоимость зависит от количества компьютеров: чем больше парк, тем ниже цена за 1 ПК.</p>
<div class="pricing-grid">
<article class="price-card">
<h3>Лайт</h3>
<p class="for">Для офисов 5-10 ПК</p>
<p class="price">от 2 500 ₽ / ПК</p>
<ul>
<li>Удалённая поддержка пользователей</li>
<li>Базовое администрирование сети</li>
<li>Установка и обновление ПО</li>
<li>Консультации по рабочим вопросам</li>
</ul>
<a href="#contact" class="btn btn-secondary">Выбрать Лайт</a>
</article>
<article class="price-card featured">
<p class="chip">Основной выбор</p>
<h3>Стандарт</h3>
<p class="for">Для офисов 10-30 ПК</p>
<p class="price">от 2 100 ₽ / ПК</p>
<ul>
<li>Всё из тарифа Лайт</li>
<li>Поддержка серверов</li>
<li>VPN для удалённых сотрудников</li>
<li>Резервное копирование</li>
<li>Мониторинг инфраструктуры</li>
<li>Плановые выезды по Татарстану</li>
<li>Базовая поставка и обновление ПО</li>
</ul>
<a href="#contact" class="btn btn-primary">Выбрать Стандарт</a>
</article>
<article class="price-card">
<h3>Про</h3>
<p class="for">Для 30+ ПК и критичных систем</p>
<p class="price">индивидуально</p>
<ul>
<li>Приоритетная поддержка</li>
<li>Расширенная стратегия бэкапов</li>
<li>SLA с фиксированными метриками</li>
<li>IT-планирование и аудит рисков</li>
<li>Повышенный уровень безопасности</li>
<li>Миграция на отечественное ПО под ключ</li>
</ul>
<a href="#contact" class="btn btn-secondary">Обсудить Про</a>
</article>
</div>
<p class="pricing-note">Точная стоимость рассчитывается после короткого аудита: учитываем количество ПК, серверов, филиалов и требования к доступности.</p>
</div>
</section>
<section class="section" id="benefits">
<div class="container">
<h2>Выгоды для бизнеса</h2>
<div class="benefits-grid">
<article><h3>Меньше простоев</h3><p>Проблемы решаются до того, как превращаются в остановку работы отдела или офиса.</p></article>
<article><h3>Понятные расходы</h3><p>Ежемесячный бюджет на IT прогнозируем и не зависит от внезапных инцидентов.</p></article>
<article><h3>Безопасность данных</h3><p>Регламенты доступа, резервные копии и контроль восстановления снижают риски потерь.</p></article>
<article><h3>Нет зависимости от одного человека</h3><p>С вами работает команда, а не один администратор, недоступный в отпуске или на больничном.</p></article>
<article><h3>IT развивается вместе с компанией</h3><p>Инфраструктура масштабируется под рост штата, филиалов и новых бизнес-задач.</p></article>
</div>
</div>
</section>
<section class="section" id="commercial">
<div class="container two-col">
<article class="panel">
<h2>Почему аутсорсинг выгоднее штатного администратора</h2>
<ul>
<li>Команда экспертов по цене одного специалиста</li>
<li>Закрываем и пользователей, и серверы, и сети</li>
<li>Нет затрат на налоги, отпускные и найм</li>
<li>Закупаем и сопровождаем нужное ПО без лишних подрядчиков</li>
<li>Фиксированный договор и прозрачные KPI</li>
</ul>
</article>
<article class="panel" id="process">
<h2>Как мы работаем</h2>
<ol>
<li><strong>Аудит:</strong> изучаем инфраструктуру, риски и узкие места.</li>
<li><strong>Обслуживание:</strong> запускаем регулярную поддержку и контроль.</li>
<li><strong>Развитие:</strong> планируем и внедряем улучшения под рост бизнеса.</li>
</ol>
</article>
</div>
<div class="container" style="margin-top:16px;">
<article class="panel">
<h3>После заключения договора</h3>
<p>Критически важные системы подключаем к нашему мониторингу. При сбоях мы узнаем о проблеме сразу, моментально начинаем работы и оперативно уведомляем заказчика о статусе и сроках восстановления.</p>
</article>
</div>
</section>
<section class="section muted" id="extra-services">
<div class="container">
<h2>Дополнительные услуги</h2>
<p class="section-subtitle">Усиливают защиту и устойчивость бизнеса, но не перегружают вашу операционку.</p>
<div class="cards two">
<article class="card">
<h3>Соответствие ФЗ-152 (персональные данные)</h3>
<ul>
<li>Аудит IT-инфраструктуры и процессов хранения данных</li>
<li>Рекомендации по защите и разграничению доступа</li>
<li>Практические шаги для снижения риска штрафов</li>
</ul>
<p>Подключается как отдельная услуга без лишней бюрократии.</p>
</article>
<article class="card">
<h3>Переход на отечественное ПО</h3>
<ul>
<li>Аудит текущего программного стека</li>
<li>Подбор российских аналогов под ваши задачи</li>
<li>Пилот, миграция и сопровождение команды</li>
</ul>
<p>Помогаем перейти спокойно и без остановки работы.</p>
</article>
</div>
</div>
</section>
<section class="section seo" id="seo-yandex">
<div class="container">
<h2>SEO-блок для Яндекс: IT-аутсорсинг в Казани и Татарстане</h2>
<p>InfraIT оказывает услуги IT-аутсорсинга для малого и среднего бизнеса: обслуживание компьютеров, серверов, сетей и корпоративных сервисов. Если вам нужно IT-обслуживание в Казани или по Татарстану с выездом инженера в день запроса, мы берём задачу в работу оперативно. Для компаний из других регионов России предоставляем удалённую IT-поддержку, мониторинг и администрирование с прозрачным SLA.</p>
<p>Мы работаем с офисами, бухгалтериями, юридическими фирмами, торговыми и сервисными компаниями, где важны стабильность, безопасность и предсказуемые расходы на IT. Отдельно сопровождаем поставку программного обеспечения и внедряем отечественные аналоги, чтобы бизнес соответствовал требованиям и развивался без технических рисков.</p>
</div>
</section>
<section class="section" id="faq">
<div class="container">
<h2>FAQ для руководителей</h2>
<div class="faq-list">
<details>
<summary>Сколько стоит IT-обслуживание в месяц?</summary>
<p>Цена зависит от количества компьютеров и состава инфраструктуры. Базово: чем больше ПК, тем ниже цена за единицу. Предварительный расчёт делаем после короткого интервью.</p>
</details>
<details>
<summary>Насколько быстро вы подключаетесь к инцидентам?</summary>
<p>Большинство заявок берём в работу в течение 15-30 минут. Для критичных задач на тарифе Про действуют приоритетные регламенты и SLA.</p>
</details>
<details>
<summary>Можно ли работать без выездов, только удалённо?</summary>
<p>Да. Для клиентов по России предоставляем полностью удалённый формат. {{ settings.geo_primary }}.</p>
</details>
<details>
<summary>Вы можете взять инфраструктуру "под ключ"?</summary>
<p>Да, это наш основной формат. От поддержки сотрудников до серверов, сетей, бэкапов и развития IT.</p>
</details>
<details>
<summary>Помогаете с поставкой ПО и переходом на российские решения?</summary>
<p>Да. Подбираем и поставляем лицензии, проводим пилот, переносим данные и поэтапно мигрируем на отечественное ПО с сопровождением сотрудников.</p>
</details>
</div>
</div>
</section>
<section class="section contact" id="contact">
<div class="container two-col">
<div>
<h2>Оставьте заявку</h2>
<p>Рассчитаем стоимость обслуживания и предложим формат поддержки под ваш бизнес за 30 минут.</p>
<ul class="contacts">
<li><strong>Телефон:</strong> <a href="tel:{{ settings.phone_link }}">{{ settings.phone_display }}</a></li>
<li><strong>Email:</strong> <a href="mailto:{{ settings.email }}">{{ settings.email }}</a></li>
<li><strong>Выезды:</strong> {{ settings.geo_primary }}</li>
<li><strong>По России:</strong> {{ settings.geo_secondary }}</li>
</ul>
</div>
<form method="post" class="lead-form" novalidate>
{% if success %}
<p class="form-success">Спасибо! Заявка отправлена. Мы свяжемся с вами в ближайшее время.</p>
{% endif %}
{% if error %}
<p class="form-error">{{ error }}</p>
{% endif %}
<label>Имя*<input type="text" name="name" required></label>
<label>Компания*<input type="text" name="company" required></label>
<label>Телефон*<input type="tel" name="phone" required></label>
<label>Email<input type="email" name="email"></label>
<label>Город<input type="text" name="city" placeholder="Казань"></label>
<label>Количество компьютеров*<input type="number" name="computers" min="1" required></label>
<label>Комментарий<textarea name="message" rows="4" placeholder="Кратко опишите текущую задачу"></textarea></label>
<button type="submit" class="btn btn-primary">Получить консультацию</button>
</form>
</div>
</section>
</main>
<a class="mobile-sticky-cta" href="#contact">Рассчитать стоимость</a>
<footer class="footer">
<div class="container footer-inner">
<div>
<a class="footer-logo" href="#top" aria-label="{{ settings.company_name }}">
<img src="{{ url_for('static', filename='img/infrait-logo.png') }}" alt="{{ settings.company_name }} logo" onerror="this.style.display='none'; this.nextElementSibling.style.display='inline-flex';">
<span class="brand-fallback">{{ settings.company_name }}</span>
</a>
<p>Комплексное IT-обслуживание бизнеса: компьютеры, серверы и сети.</p>
</div>
<div>
<p><a href="tel:{{ settings.phone_link }}">{{ settings.phone_display }}</a></p>
<p><a href="mailto:{{ settings.email }}">{{ settings.email }}</a></p>
</div>
</div>
</footer>
{% endblock %}