init infrait
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal 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
2
.env.example
Normal 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
17
Dockerfile
Normal 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"]
|
||||||
BIN
__pycache__/app.cpython-314.pyc
Normal file
BIN
__pycache__/app.cpython-314.pyc
Normal file
Binary file not shown.
255
app.py
Normal file
255
app.py
Normal 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
BIN
data/infra.db
Normal file
Binary file not shown.
2
data/leads.csv
Normal file
2
data/leads.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
created_at,name,company,phone,email,city,computers,message
|
||||||
|
2026-02-12T16:33:08,Иван,ООО Тест,123,,,10,
|
||||||
|
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal 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
14
gunicorn_conf.py
Normal 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
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Flask==3.1.0
|
||||||
|
gunicorn==23.0.0
|
||||||
559
static/css/styles.css
Normal file
559
static/css/styles.css
Normal 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
4
static/img/README.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Положите файл логотипа сюда:
|
||||||
|
static/img/infrait-logo.png
|
||||||
|
|
||||||
|
Текущий шаблон уже подключен к этому пути.
|
||||||
BIN
static/img/infrait-logo.png
Normal file
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
23
static/js/main.js
Normal 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);
|
||||||
|
});
|
||||||
|
})();
|
||||||
27
templates/admin_login.html
Normal file
27
templates/admin_login.html
Normal 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>
|
||||||
77
templates/admin_settings.html
Normal file
77
templates/admin_settings.html
Normal 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
37
templates/base.html
Normal 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
339
templates/index.html
Normal 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": "Большинство обращений берём в работу в течение 15–30 минут. Для критичных инцидентов действует приоритетная схема в зависимости от тарифа."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@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 %}
|
||||||
Reference in New Issue
Block a user