Compare commits
17
Commits
0c4ad38f84
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
978e5116f2 | ||
|
|
74161c415d | ||
|
|
9c3dd53f41 | ||
|
|
4d5e4cf86d | ||
|
|
1c2d0de14c | ||
|
|
2e11b536cb | ||
|
|
c7ed8235ed | ||
|
|
0773b25064 | ||
|
|
a729bcf9fb | ||
|
|
49542cd49c | ||
|
|
28556f0b12 | ||
|
|
8c682d3fef | ||
|
|
6122951614 | ||
|
|
5d0f2eb1b2 | ||
|
|
f47b2ca364 | ||
|
|
ab9b720326 | ||
|
|
0d0e901404 |
@@ -0,0 +1,51 @@
|
|||||||
|
# Исключаем мусор и локальные артефакты, чтобы Docker-контекст был компактным.
|
||||||
|
|
||||||
|
# Git и IDE-файлы в образ не нужны.
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Секреты и локальные настройки не должны попадать в контейнерный контекст.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.env.sample
|
||||||
|
|
||||||
|
# Виртуальное окружение и служебные артефакты Python.
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.log
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.coverage*
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
|
||||||
|
# Локальные базы и дампы SQLite в контейнер не тащим.
|
||||||
|
*.sqlite3
|
||||||
|
database/
|
||||||
|
media/
|
||||||
|
|
||||||
|
# Локальная сборка фронтенда пока не нужна в Docker-контексте.
|
||||||
|
# Если позже соберём frontend внутри Docker, это правило можно пересмотреть.
|
||||||
|
frontend-assembly/
|
||||||
|
|
||||||
|
# Загруженные медиа-файлы монтируются отдельно и не должны раздувать контекст.
|
||||||
|
public/media/
|
||||||
|
|
||||||
|
# Документация и служебные git-ignore-файлы не нужны в runtime-образе.
|
||||||
|
*.md
|
||||||
|
**/.gitignore
|
||||||
|
|
||||||
|
# Репозиторные и оркестрационные файлы не нужны внутри runtime-образа.
|
||||||
|
.gitea/
|
||||||
|
.junie/
|
||||||
|
_blueprint/
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# В принцепе, Dockerfile тоже можно исключить из контейнера, но для BuildKit это иногда ломает сборку
|
||||||
|
# и считается антипаттерном, а так же может приводить к предупреждениям при сборке.
|
||||||
|
# Dockerfile
|
||||||
|
|
||||||
+9
-3
@@ -2,10 +2,16 @@
|
|||||||
# Скопируй этот файл в `.env` и заполни реальными значениями.
|
# Скопируй этот файл в `.env` и заполни реальными значениями.
|
||||||
|
|
||||||
DJANGO_DEBUG=True
|
DJANGO_DEBUG=True
|
||||||
DJANGO_SECRET_KEY=CHANGE_ME
|
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,hypn0.xyz
|
||||||
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,hypn0.ru
|
|
||||||
DJANGO_ADMINS=hypn0:admin@example.com
|
DJANGO_ADMINS=hypn0:admin@example.com
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,https://hypn0.ru
|
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,https://hypn0.xyz
|
||||||
|
|
||||||
|
# ====== Получить код для Django:
|
||||||
|
# poetry run python hypn0/manage.py shell
|
||||||
|
# Затем выполнить две строчки кода:
|
||||||
|
# from django.core.management.utils import get_random_secret_key
|
||||||
|
# print(get_random_secret_key())
|
||||||
|
DJANGO_SECRET_KEY=CHANGE_ME
|
||||||
|
|
||||||
# Имя файла SQLite-базы. Путь всегда собирается через `BASE_DIR.parent / 'database'`.
|
# Имя файла SQLite-базы. Путь всегда собирается через `BASE_DIR.parent / 'database'`.
|
||||||
DJANGO_SQLITE_NAME=hypn0-db.sqlite3
|
DJANGO_SQLITE_NAME=hypn0-db.sqlite3
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: HYPN0 Build and Push Docker Image
|
||||||
|
run-name: HYPN0 Build and Push Docker Image ${{ github.ref_name }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Запускать сборку только при создании тега, начинающегося с 'v' (например, v1.0.0, v2.3.1). Например:
|
||||||
|
# git tag v0.1.1 && git push origin v0.1.1
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.cube2.ru
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest # Или метка вашего раннера, если он специфичный (например, macos или self-hosted)
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Настройка QEMU для мультиплатформенной сборки (если нужно собирать под разные архитектуры)
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
# Настройка Docker Buildx (обязательно для build-push-action)
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
# Логин в реестр Gitea
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
# Извлечение метаданных (тегов и лейблов) для Docker
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=latest,enable=${{ github.ref_type == 'tag' }}
|
||||||
|
|
||||||
|
# Сборка и отправка образа
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
# Собираем под текущую архитектуру (linux/amd64).
|
||||||
|
# Если сервер и MacMini на разных архитектурах (x86 vs ARM), добавьте нужные, например: linux/amd64,linux/arm64
|
||||||
|
# platforms: linux/amd64,linux/arm64
|
||||||
|
# ---
|
||||||
|
# Собираем только под linux/amd64 (для скорости)
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
# ДОБАВЛЕНО для оптимизации сборки (кэш слоёв в реестре):
|
||||||
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||||
|
timeout: 1800 # Увеличено до 30 минут на всю сборку
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
# =================================================
|
||||||
|
# STAGE 1: Builder - Установка зависимостей
|
||||||
|
# =================================================
|
||||||
|
FROM python:3.14-slim AS builder
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PIP_DEFAULT_TIMEOUT=100
|
||||||
|
|
||||||
|
# ENV POETRY_VERSION=2.1.1
|
||||||
|
# ENV POETRY_HOME="/opt/poetry"
|
||||||
|
# ENV POETRY_NO_INTERACTION=1
|
||||||
|
# ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||||
|
|
||||||
|
# Устанавливаем Poetry
|
||||||
|
RUN pip install --no-cache-dir --default-timeout=100 --retries 10 poetry poetry-plugin-export
|
||||||
|
|
||||||
|
# Создаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем только файлы зависимостей для кэширования этого слоя
|
||||||
|
COPY pyproject.toml poetry.lock /app/
|
||||||
|
|
||||||
|
# Экспортируем lock-файл в requirements.txt и ставим зависимости через pip.
|
||||||
|
# Это обычно быстрее и проще для Docker, чем полноценная установка через Poetry.
|
||||||
|
RUN poetry export --format requirements.txt --without-hashes --with dev --output /tmp/requirements.txt \
|
||||||
|
&& pip install --no-cache-dir --default-timeout=100 --retries 10 -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# STAGE 2: Final - Создание чистого и безопасного образа
|
||||||
|
# =================================================
|
||||||
|
FROM python:3.14-slim AS stage-final
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV DJANGO_SETTINGS_MODULE=hypn0.settings
|
||||||
|
ENV PYTHONPATH="/home/app/web/hypn0"
|
||||||
|
ENV HOME="/home/app"
|
||||||
|
|
||||||
|
|
||||||
|
# Создаем пользователя без прав root для безопасности
|
||||||
|
# RUN addgroup --system app && adduser --system --ingroup app app
|
||||||
|
|
||||||
|
# Создаем рабочую директорию
|
||||||
|
WORKDIR /home/app/web
|
||||||
|
|
||||||
|
# Копируем установленные Python-пакеты из builder-стадии
|
||||||
|
# Каталог /usr/local/bin нужен для бинарных файлов, таких как gunicorn, pytest, whitenoise и т.п.
|
||||||
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
#Каталог /usr/local/lib/python3.14/site-packages нужен для обычных Python-пакетов и батареек
|
||||||
|
COPY --from=builder /usr/local/lib/python3.14/site-packages /usr/local/lib/python3.14/site-packages
|
||||||
|
|
||||||
|
# Копируем исходный код проекта и устанавливаем правильного владельца
|
||||||
|
# ИЗМЕНЕНИЕ: app:app -> 1000:1000
|
||||||
|
COPY --chown=1000:1000 . .
|
||||||
|
|
||||||
|
# 1. Создаём директорию для конфигов nginx и даём права пользователю app
|
||||||
|
# Это выполняется ещё от root, поэтому проблем с permissions не будет.
|
||||||
|
# 2. Создаём директорию для собранной статики и даём права пользователю app.
|
||||||
|
# `STATIC_ROOT` в settings.py живёт внутри `public`.
|
||||||
|
# 3. Создаём директорию для ошибок (404, 500) и даём права пользователю app
|
||||||
|
# 4. Создаём директорию для БД и даём права пользователю app
|
||||||
|
# Это важно когда БД монтируется как том с хоста
|
||||||
|
RUN mkdir -p /home/app \
|
||||||
|
/nginx_configs_host/nginx \
|
||||||
|
/home/app/web/public/staticfiles \
|
||||||
|
/home/app/web/public/media/_error \
|
||||||
|
/home/app/web/database && \
|
||||||
|
chown -R 1000:1000 /nginx_configs_host /home/app/web/public /home/app/web/database
|
||||||
|
|
||||||
|
# Переключаемся на пользователя без прав root
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
|
||||||
|
# Собираем статику
|
||||||
|
# Используем dummy ключ, так как .env файла нет на этапе сборки
|
||||||
|
RUN SECRET_KEY=dummy python hypn0/manage.py collectstatic --noinput --clear
|
||||||
|
|
||||||
|
# Открываем порт
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Проверка здоровья контейнера
|
||||||
|
# Docker будет периодически проверять, жив ли контейнер, отправляя GET запрос к главной странице.
|
||||||
|
# Параметры:
|
||||||
|
# --interval=30s - проверка каждые 30 секунд
|
||||||
|
# --timeout=3s - ожидаем ответ максимум 3 секунды
|
||||||
|
# --start-period=10s - даем контейнеру 10 секунд на запуск перед первой проверкой
|
||||||
|
# --retries=3 - объявляем контейнер unhealthy после 3 неудачных попыток
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()" || exit 1
|
||||||
|
|
||||||
|
# Переходим в директорию с manage.py для корректного запуска gunicorn
|
||||||
|
WORKDIR /home/app/web/hypn0
|
||||||
|
|
||||||
|
# Команда запуска (два воркера для лучшей производительности, можно увеличить до число ядер на хосте)
|
||||||
|
CMD ["python", "-m", "gunicorn", "--workers", "2", "--bind", "0.0.0.0:8000", "hypn0.wsgi:application"]
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# config/nginx/hypn0-app--external-nginx.conf
|
||||||
|
# ==============================================================================
|
||||||
|
# ЭТАЛОННЫЙ КОНФИГУРАЦИОННЫЙ ФАЙЛ NGINX (Reverse Proxy для Docker)
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# ВНИМАНИЕ:
|
||||||
|
# Этот файл является шаблоном. При первом деплое он копируется в `/home/user/app/hypn0-site/config/nginx/hypn0-app--external-nginx.conf`,
|
||||||
|
# а затем (уже руками) через силинк в `/etc/nginx/sites-available/` и активируется.
|
||||||
|
# При последующих деплоях он НЕ ПЕРЕЗАПИСЫВАЕТСЯ автоматически, чтобы не затереть SSL-сертификаты и ручные правки.
|
||||||
|
#
|
||||||
|
# Если вы изменили этот файл в репозитории и хотите применить изменения на проде:
|
||||||
|
# вам нужно обновить файл в `/home/user/app/hypn0-site/config/nginx/hypn0-app--external-nginx.conf` вручную (diff + copy).
|
||||||
|
#
|
||||||
|
# Так же (рядом) будет создан образец этого файла `nginx_hypn0.conf.example`, который будет обновляться при деплоях
|
||||||
|
# из репозитория, чтобы вы могли видеть, что изменилось и при необходимости перенести эти изменения на прод.
|
||||||
|
#
|
||||||
|
# Предполагаемая структура на сервере:
|
||||||
|
# /home/user/app/hypn0-site/
|
||||||
|
# ├── docker-compose.yml
|
||||||
|
# ├── .env
|
||||||
|
# ├── media/ <-- Сюда Nginx смотрит напрямую (Docker volume)
|
||||||
|
# └── ...
|
||||||
|
|
||||||
|
# 1. Описываем, где живет наш Django в Docker
|
||||||
|
upstream hypn0-django {
|
||||||
|
# Мы пробрасываем порт 8042 из контейнера наружу (в docker-compose.yml имя сервиса 'web', контейнер 'hypn0-backend')
|
||||||
|
server 127.0.0.1:8042;
|
||||||
|
keepalive_requests 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Конфигурируем сервер
|
||||||
|
server {
|
||||||
|
server_name hypn0.xyz; # Основное доменное имя
|
||||||
|
|
||||||
|
# Слушаем 80 порт (Certbot потом добавит сюда редирект на 443 и настройки SSL)
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок
|
||||||
|
|
||||||
|
# Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu)
|
||||||
|
access_log /var/log/nginx/hypn0.access.log;
|
||||||
|
error_log /var/log/nginx/hypn0.error.log;
|
||||||
|
|
||||||
|
# --- GZIP (Сжатие) ---
|
||||||
|
# Очень важно для динамического HTML от Django, который Gunicorn отдает несжатым.
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on; # Добавляет заголовок Vary: Accept-Encoding
|
||||||
|
gzip_proxied any; # Сжимать ответы, даже если мы за прокси
|
||||||
|
gzip_comp_level 6; # Оптимальный баланс скорость/сжатие
|
||||||
|
gzip_min_length 1000; # Не сжимать совсем мелочь
|
||||||
|
# Типы файлов для сжатия (HTML сжимается автоматически, его писать не нужно)
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
application/xml+rss
|
||||||
|
image/svg+xml
|
||||||
|
image/x-icon
|
||||||
|
application/vnd.ms-fontobject
|
||||||
|
font/woff
|
||||||
|
font/woff2;
|
||||||
|
|
||||||
|
# --- МЕДИА ФАЙЛЫ (Загруженный контент) ---
|
||||||
|
# Nginx отдает их напрямую с диска хоста, не дергая Docker.
|
||||||
|
# Путь должен совпадать с тем, где лежит volume на хост-машине.
|
||||||
|
# ВАЖНО: Убедитесь, что пользователь nginx (www-data) имеет права на чтение этой папки!
|
||||||
|
# ТРЕБУЕТСЯ ЗАМЕНА ПРИ ДЕПЛОЕ: /home/user/app/hypn0-site -> ваш реальный путь
|
||||||
|
location /media/ {
|
||||||
|
alias /home/user/app/hypn0-site/media/;
|
||||||
|
expires 30d; # Кешируем картинки на месяц
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) ---
|
||||||
|
# Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML.
|
||||||
|
# Эти файлы должны лежать в папке, доступной Nginx (например, в `media/_error`).
|
||||||
|
#
|
||||||
|
# ВАЖНО:
|
||||||
|
# 1. Файлы 50x.html (500, 502, 503, 504) копируются в `media/_error` при старте контейнера (см. docker-compose.prod-test.yml -> command).
|
||||||
|
# 2. error_page директива перехватывает ошибки от апстрима (Gunicorn).
|
||||||
|
error_page 500 /500.html;
|
||||||
|
error_page 502 /502.html;
|
||||||
|
error_page 503 /503.html;
|
||||||
|
error_page 504 /504.html;
|
||||||
|
|
||||||
|
location = /500.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /502.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /503.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /504.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
|
||||||
|
# 404 (и другие) тоже нужно кастомизировать... обычно Django сам отдает 404.
|
||||||
|
# Но, например, Nginx отдаст 404 при ошике доступа к media-файлам (они храняться на хосте, а не в контейнере)
|
||||||
|
error_page 400 /400.html;
|
||||||
|
error_page 401 /401.html;
|
||||||
|
error_page 403 /403.html;
|
||||||
|
error_page 404 /404.html;
|
||||||
|
error_page 413 /413.html;
|
||||||
|
error_page 429 /429.html;
|
||||||
|
|
||||||
|
location = /400.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /401.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /403.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /404.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /413.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
location = /429.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
|
||||||
|
# Указываем единую страницу (на реконструкции) для всех прочих ошибок
|
||||||
|
error_page 405 406 407 408 409 410 411 412 414 415 416 417 418 421 422 423 424 425 426 428 431 451 /under_reconstruction.html;
|
||||||
|
location = /under_reconstruction.html { root /home/user/app/hypn0-site/media/_error; internal; }
|
||||||
|
|
||||||
|
# --- ВСЁ ОСТАЛЬНОЕ (Django + WhiteNoise) ---
|
||||||
|
# Статика (/static/), robots.txt, favicon.ico и сам сайт обрабатываются внутри контейнера.
|
||||||
|
# Nginx просто прокидывает запрос внутрь.
|
||||||
|
location / {
|
||||||
|
proxy_pass http://hypn0-django;
|
||||||
|
|
||||||
|
# Передаем правильные заголовки, чтобы Django знал реальный IP и протокол
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# Если нужно чтобы Django обрабатывал и HTTP, и HTTPS, то можно раскомментировать эту строку
|
||||||
|
# и передавать реальный протокол от клиента
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Явно указываем https, потому что клиент всегда приходит по HTTPS к Nginx
|
||||||
|
# Даже если внутри контейнера это HTTP на 127.0.0.1:8042, для Django это должно быть HTTPS
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
|
||||||
|
# Тайм-ауты (важно для долгих операций, если они есть)
|
||||||
|
proxy_read_timeout 180s;
|
||||||
|
proxy_connect_timeout 180s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Редирект с www на без-www (SEO best practice)
|
||||||
|
# server {
|
||||||
|
# server_name www.hypn0.ru;
|
||||||
|
# listen 80;
|
||||||
|
# return 301 $scheme://hypn0.ru$request_uri; # Всегда редиректим на основной домен
|
||||||
|
# }
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Docker Compose для РАЗРАБОТКИ (Local Development)
|
||||||
|
# Этот файл содержит настройки для локальной работы (live reload, debug).
|
||||||
|
# Запуск: docker compose -f docker-compose.local up --build
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
# Имя контейнера для удобства
|
||||||
|
container_name: hypn0-backend-dev
|
||||||
|
|
||||||
|
# Сборка из текущей директории
|
||||||
|
build: .
|
||||||
|
|
||||||
|
# Проброс портов (чтобы сайт был доступен на localhost:8042)
|
||||||
|
ports:
|
||||||
|
- "8042:8000"
|
||||||
|
|
||||||
|
# 1. КОМАНДА ЗАПУСКА (Dev режим)
|
||||||
|
# 1.A. Запускаем миграции, чтобы база была актуальной.
|
||||||
|
# 1.B. Запускаем кастомную команду rehash, чтобы обновить id-хэши для SVG-генераций в соответствии
|
||||||
|
# с HASHIDS_SALT из текущего .env
|
||||||
|
# 1.C. Запускаем Gunicorn с 1 воркером и включаем --reload для авто-перезагрузки при изменении кода.
|
||||||
|
# Уменьшаем число воркеров до 1 (ресурсы dev-машины можно не экономит, но одного достаточно).
|
||||||
|
# Убираем collectstatic (в dev Django сам может отдавать статику или она нам не так важна сжатой)
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py migrate --noinput &&
|
||||||
|
python manage.py rehash &&
|
||||||
|
python -m gunicorn --workers 1 --bind 0.0.0.0:8000 --reload hypn0.wsgi:application"
|
||||||
|
|
||||||
|
# 2. МОНТИРОВАНИЕ КОДА (Live Reload)
|
||||||
|
# Подключаем локальные папки внутрь контейнера, чтобы Gunicorn видел изменения без пересборки образа.
|
||||||
|
volumes:
|
||||||
|
# Монтируем основной код проекта.
|
||||||
|
# Так как web, templates и manage.py лежат внутри hypn0/, одного этого маунта достаточно.
|
||||||
|
- ./hypn0:/home/app/web/hypn0
|
||||||
|
|
||||||
|
# Монтируем всю папку public (Static + Media)
|
||||||
|
# Это нужно, чтобы:
|
||||||
|
# 1. Изменения в CSS/JS (public/static) сразу были видны (Live Reload).
|
||||||
|
# 2. Загруженные картинки (public/media) сохранялись на диске.
|
||||||
|
- ./public:/home/app/web/public
|
||||||
|
|
||||||
|
# Монтируем базу данных (чтобы данные сохранялись при пересоздании контейнера)
|
||||||
|
# Используем ту же папку database, что и на проде, для единообразия.
|
||||||
|
# ВАЖНО: Django ищет базу в BASE_DIR.parent / 'database/db.sqlite3'
|
||||||
|
# В контейнере BASE_DIR=/home/app/web/hypn0, значит путь к базе: /home/app/web/database/db.sqlite3
|
||||||
|
- ./database:/home/app/web/database
|
||||||
|
|
||||||
|
# 3. ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ
|
||||||
|
env_file:
|
||||||
|
# файл с переменными окружения для разработки
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# на всякий случай, принудительно включаем DEBUG и DEBUG-уровень логов (вдруг в .env что-то не так)
|
||||||
|
# - DJANGO_DEBUG=False
|
||||||
|
- DJANGO_DEBUG=True
|
||||||
|
- DJANGO_LOG_LEVEL=DEBUG
|
||||||
|
# В dev нам не нужно ограничивать буферизацию так строго, но не помешает.
|
||||||
|
|
||||||
|
# 4. РЕСУРСЫ (Без лимитов для разработки)
|
||||||
|
# Удаляем секцию ограничений, чтобы локально использовать все доступные ресурсы хоста.
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# limits:
|
||||||
|
# cpus: ...
|
||||||
|
# memory: ...
|
||||||
|
# mem_limit: ...
|
||||||
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Docker Compose для PRODUCTION
|
||||||
|
# Этот файл запускается на боевом сервере.
|
||||||
|
# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d
|
||||||
|
# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod-test.yml up -d
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# В новой версии Docker не нужно
|
||||||
|
# version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# --- СБОРКА ТАИЛВИНД CSS (Фронтенд-компилятор) ---
|
||||||
|
# Запускается один раз при старте, собирает свежий tailwind.min.css и завершает работу
|
||||||
|
tailwind-builder:
|
||||||
|
image: node:22-slim
|
||||||
|
container_name: hypn0-tailwind-builder
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
command: bash ./scripts/build-tailwind.sh
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
# --- ОCНОВНОЙ СЕРВИС: DJANGO + GUNICORN + WHITENOISE ---
|
||||||
|
web:
|
||||||
|
# Имя контейнера
|
||||||
|
container_name: hypn0-backend
|
||||||
|
|
||||||
|
# Ждём успешного завершения компиляции стилей перед запуском бэкенда
|
||||||
|
depends_on:
|
||||||
|
tailwind-builder:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
# 1. ОБРАЗ
|
||||||
|
# В продакшене мы используем готовый, собранный образ из реестра (Gitea)
|
||||||
|
# image: git.cube2.ru/erjemin/2026-hypn0-site:latest
|
||||||
|
# Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
|
||||||
|
build: .
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# 2. Метки для Watchtower (авто-обновление)
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.scope=hypn0-scope"
|
||||||
|
|
||||||
|
# 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh)
|
||||||
|
# Выполняем цепочку команд внутри контейнера при запуске:
|
||||||
|
# a. Миграции
|
||||||
|
# b. Collectstatic
|
||||||
|
# c. рехеш-id
|
||||||
|
# d. Создаем папку nginx в примонтированном томе конфигов (если нет)
|
||||||
|
# e. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем
|
||||||
|
# через переменную окружения HOST_PROJECT_PATH)
|
||||||
|
# f. Инициализация боевого конфига (если нет)
|
||||||
|
# g. Создаем папку для ошибок и копируем туда HTML error pages и их ассеты (там их увидит Nginx хоста)
|
||||||
|
# — иконки, SVG-иллюстрации и under_reconstruction тоже должны лежать рядом, чтобы Nginx мог их отдать
|
||||||
|
# h. Запуск Gunicorn
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py migrate --noinput &&
|
||||||
|
python manage.py collectstatic --noinput --clear &&
|
||||||
|
python manage.py rehash &&
|
||||||
|
mkdir -p /nginx_configs_host/nginx &&
|
||||||
|
sed \"s|/home/user/app/hypn0-site|${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}|g\" /home/app/web/config/nginx/hypn0-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_hypn0.conf.example &&
|
||||||
|
if [ ! -f /nginx_configs_host/nginx/hypn0-app--external-nginx.conf ]; then
|
||||||
|
cp /nginx_configs_host/nginx/nginx_hypn0.conf.example /nginx_configs_host/nginx/hypn0-app--external-nginx.conf;
|
||||||
|
echo 'INIT: Created new nginx config with correct paths';
|
||||||
|
fi &&
|
||||||
|
ERROR_DIR=/home/app/web/public/media/_error &&
|
||||||
|
mkdir -p "$$ERROR_DIR" &&
|
||||||
|
for code in 400 401 403 404 413 429 500 502 503 504; do
|
||||||
|
cp /home/app/web/hypn0/templates/_error/$${code}.html "$$ERROR_DIR/$${code}.html";
|
||||||
|
done &&
|
||||||
|
cp /home/app/web/hypn0/templates/_error/under_reconstruction.html "$$ERROR_DIR/under_reconstruction.html" &&
|
||||||
|
cp /home/app/web/public/static/img/favicon.svg "$$ERROR_DIR/favicon.svg" &&
|
||||||
|
cp /home/app/web/public/static/img/favicon.ico "$$ERROR_DIR/favicon.ico" &&
|
||||||
|
cp /home/app/web/public/static/img/logo-hypn0.svg "$$ERROR_DIR/logo-hypn0.svg" &&
|
||||||
|
cp /home/app/web/public/static/img/thinking.svg "$$ERROR_DIR/thinking.svg" &&
|
||||||
|
echo '*/30 * * * * cd '${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}' && docker compose exec -T web python manage.py rescore >> /var/log/hypn0-rescore.log 2>&1' > /nginx_configs_host/crontab.example &&
|
||||||
|
echo '' &&
|
||||||
|
echo '========================================================================' &&
|
||||||
|
echo ' [HYPN0 CRON NOTICE] Слава Гипножабе! Не забудь настроить cron на хосте:' &&
|
||||||
|
echo ' Добавь строку в `crontab -e` (или см. config/crontab.example):' &&
|
||||||
|
echo ' */30 * * * * cd '${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}' && docker compose exec -T web python manage.py rescore' &&
|
||||||
|
echo '========================================================================' &&
|
||||||
|
echo '' &&
|
||||||
|
python -m gunicorn --workers 2 --bind 0.0.0.0:8000 hypn0.wsgi:application"
|
||||||
|
|
||||||
|
# 4. Проброс портов (Внешний Nginx -> localhost:8042)
|
||||||
|
ports:
|
||||||
|
# Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
|
||||||
|
- "127.0.0.1:8042:8000"
|
||||||
|
|
||||||
|
# 5. Тома (Volumes)
|
||||||
|
volumes:
|
||||||
|
# База данных
|
||||||
|
# Монтируем папку database с хоста в папку с базой внутри контейнера.
|
||||||
|
# Путь в контейнере: /home/app/web/database (так как Django ищет базу в BASE_DIR.parent/database)
|
||||||
|
- ./database:/home/app/web/database
|
||||||
|
|
||||||
|
# Медиа, служебные error-pages и их ассеты лежат в папке `media` на хосте.
|
||||||
|
- ./media:/home/app/web/public/media
|
||||||
|
|
||||||
|
# Конфиги (Монтируем папку ./config с хоста в /nginx_configs_host внутри контейнера)
|
||||||
|
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
||||||
|
- ./config:/nginx_configs_host
|
||||||
|
|
||||||
|
# 6. Пользователь и права
|
||||||
|
# На первом старте контейнеру нужны права на инициализацию bind-mount'ов
|
||||||
|
# (media/config/database) на хосте, иначе `mkdir` для `media/_error` падает с Permission denied.
|
||||||
|
# После первичной инициализации можно будет отдельно ужесточить права на уровне хоста.
|
||||||
|
user: "0:0"
|
||||||
|
|
||||||
|
# Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root.
|
||||||
|
# cap_add:
|
||||||
|
# - SYS_PTRACE
|
||||||
|
|
||||||
|
# 7. Переменные окружения
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=hypn0.settings
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||||
|
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}
|
||||||
|
|
||||||
|
# 8. Проверка здоровья контейнера (Healthcheck)
|
||||||
|
# Docker будет периодически проверять статус контейнера. Это критично для Watchtower!
|
||||||
|
# Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый.
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"]
|
||||||
|
interval: 3m # Проверка каждые 3 минуты
|
||||||
|
timeout: 12s # Таймаут ответа - 12 секунды
|
||||||
|
start_period: 20s # Даем 20 секунд на стартап перед первой проверкой
|
||||||
|
retries: 3 # Unhealthy после 3 неудачных попыток
|
||||||
|
|
||||||
|
# 9. Логирование (Ротация)
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# 10. Ресурсы
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.50'
|
||||||
|
memory: 512M
|
||||||
|
mem_limit: 512m
|
||||||
|
|
||||||
|
# --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ ---
|
||||||
|
# Следит за реестром Gitea и обновляет контейнер web, если появился новый image
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: hypn0_watchtower
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
# Токен/Логин для вашего приватного реестра (нужно добавить в .env!)
|
||||||
|
# REPO_USER и REPO_PASS должны быть в .env файле на сервере
|
||||||
|
- REPO_USER=${REPO_USER}
|
||||||
|
- REPO_PASS=${REPO_PASS}
|
||||||
|
- WATCHTOWER_SCOPE=hypn0-scope
|
||||||
|
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
|
||||||
|
- DOCKER_API_VERSION=1.44
|
||||||
|
# Дополнительные опции для правильной работы с healthcheck
|
||||||
|
- WATCHTOWER_WAIT_ON_TIMEOUT=60 # Ждем 60 сек пока контейнер станет healthy перед финализацией
|
||||||
|
- WATCHTOWER_LIFECYCLE_HOOKS=true # Включаем lifecycle hooks для graceful shutdown
|
||||||
|
command: --interval 1800 --cleanup # Проверять каждые 30 минут
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Docker Compose для PRODUCTION
|
||||||
|
# Этот файл запускается на боевом сервере.
|
||||||
|
# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d
|
||||||
|
# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod-test.yml up -d
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# В новой версии Docker не нужно
|
||||||
|
# version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# --- СБОРКА ТАИЛВИНД CSS (Фронтенд-компилятор) ---
|
||||||
|
# Запускается один раз при старте, собирает свежий tailwind.min.css и завершает работу
|
||||||
|
tailwind-builder:
|
||||||
|
image: node:22-slim
|
||||||
|
container_name: hypn0-tailwind-builder
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
command: bash ./scripts/build-tailwind.sh
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
# --- ОCНОВНОЙ СЕРВИС: DJANGO + GUNICORN + WHITENOISE ---
|
||||||
|
web:
|
||||||
|
# Имя контейнера
|
||||||
|
container_name: hypn0-backend
|
||||||
|
|
||||||
|
# Ждём успешного завершения компиляции стилей перед запуском бэкенда
|
||||||
|
depends_on:
|
||||||
|
tailwind-builder:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
# 1. ОБРАЗ
|
||||||
|
# В продакшене мы используем готовый, собранный образ из реестра (Gitea)
|
||||||
|
image: git.cube2.ru/erjemin/2026-hypn0-site:latest
|
||||||
|
# Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
|
||||||
|
# build: .
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# 2. Метки для Watchtower (авто-обновление)
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.scope=hypn0-scope"
|
||||||
|
|
||||||
|
# 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh)
|
||||||
|
# Выполняем цепочку команд внутри контейнера при запуске:
|
||||||
|
# a. Миграции
|
||||||
|
# b. Collectstatic
|
||||||
|
# c. рехеш-id
|
||||||
|
# d. Создаем папку nginx в примонтированном томе конфигов (если нет)
|
||||||
|
# e. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем
|
||||||
|
# через переменную окружения HOST_PROJECT_PATH)
|
||||||
|
# f. Инициализация боевого конфига (если нет)
|
||||||
|
# g. Создаем папку для ошибок и копируем туда HTML error pages и их ассеты (там их увидит Nginx хоста)
|
||||||
|
# — иконки, SVG-иллюстрации и under_reconstruction тоже должны лежать рядом, чтобы Nginx мог их отдать
|
||||||
|
# h. Запуск Gunicorn
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py migrate --noinput &&
|
||||||
|
python manage.py collectstatic --noinput --clear &&
|
||||||
|
python manage.py rehash &&
|
||||||
|
mkdir -p /nginx_configs_host/nginx &&
|
||||||
|
sed \"s|/home/user/app/hypn0-site|${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}|g\" /home/app/web/config/nginx/hypn0-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_hypn0.conf.example &&
|
||||||
|
if [ ! -f /nginx_configs_host/nginx/hypn0-app--external-nginx.conf ]; then
|
||||||
|
cp /nginx_configs_host/nginx/nginx_hypn0.conf.example /nginx_configs_host/nginx/hypn0-app--external-nginx.conf;
|
||||||
|
echo 'INIT: Created new nginx config with correct paths';
|
||||||
|
fi &&
|
||||||
|
ERROR_DIR=/home/app/web/public/media/_error &&
|
||||||
|
mkdir -p "$$ERROR_DIR" &&
|
||||||
|
for code in 400 401 403 404 413 429 500 502 503 504; do
|
||||||
|
cp /home/app/web/hypn0/templates/_error/$${code}.html "$$ERROR_DIR/$${code}.html";
|
||||||
|
done &&
|
||||||
|
cp /home/app/web/hypn0/templates/_error/under_reconstruction.html "$$ERROR_DIR/under_reconstruction.html" &&
|
||||||
|
cp /home/app/web/public/static/img/favicon.svg "$$ERROR_DIR/favicon.svg" &&
|
||||||
|
cp /home/app/web/public/static/img/favicon.ico "$$ERROR_DIR/favicon.ico" &&
|
||||||
|
cp /home/app/web/public/static/img/logo-hypn0.svg "$$ERROR_DIR/logo-hypn0.svg" &&
|
||||||
|
cp /home/app/web/public/static/img/thinking.svg "$$ERROR_DIR/thinking.svg" &&
|
||||||
|
echo '*/30 * * * * cd '${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}' && docker compose exec -T web python manage.py rescore >> /var/log/hypn0-rescore.log 2>&1' > /nginx_configs_host/crontab.example &&
|
||||||
|
echo '' &&
|
||||||
|
echo '========================================================================' &&
|
||||||
|
echo ' [HYPN0 CRON NOTICE] Слава Гипножабе! Не забудь настроить cron на хосте:' &&
|
||||||
|
echo ' Добавь строку в `crontab -e` (или см. config/crontab.example):' &&
|
||||||
|
echo ' */30 * * * * cd '${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}' && docker compose exec -T web python manage.py rescore' &&
|
||||||
|
echo '========================================================================' &&
|
||||||
|
echo '' &&
|
||||||
|
python -m gunicorn --workers 2 --bind 0.0.0.0:8000 hypn0.wsgi:application"
|
||||||
|
|
||||||
|
# 4. Проброс портов (Внешний Nginx -> localhost:8042)
|
||||||
|
ports:
|
||||||
|
# Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
|
||||||
|
- "127.0.0.1:8042:8000"
|
||||||
|
|
||||||
|
# 5. Тома (Volumes)
|
||||||
|
volumes:
|
||||||
|
# База данных
|
||||||
|
# Монтируем папку database с хоста в папку с базой внутри контейнера.
|
||||||
|
# Путь в контейнере: /home/app/web/database (так как Django ищет базу в BASE_DIR.parent/database)
|
||||||
|
- ./database:/home/app/web/database
|
||||||
|
|
||||||
|
# Медиа, служебные error-pages и их ассеты лежат в папке `media` на хосте.
|
||||||
|
- ./media:/home/app/web/public/media
|
||||||
|
|
||||||
|
# Конфиги (Монтируем папку ./config с хоста в /nginx_configs_host внутри контейнера)
|
||||||
|
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
||||||
|
- ./config:/nginx_configs_host
|
||||||
|
|
||||||
|
# 6. Пользователь и права
|
||||||
|
# На первом старте контейнеру нужны права на инициализацию bind-mount'ов
|
||||||
|
# (media/config/database) на хосте, иначе `mkdir` для `media/_error` падает с Permission denied.
|
||||||
|
# После первичной инициализации можно будет отдельно ужесточить права на уровне хоста.
|
||||||
|
user: "0:0"
|
||||||
|
|
||||||
|
# Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root.
|
||||||
|
# cap_add:
|
||||||
|
# - SYS_PTRACE
|
||||||
|
|
||||||
|
# 7. Переменные окружения
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=hypn0.settings
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||||
|
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}
|
||||||
|
|
||||||
|
# 8. Проверка здоровья контейнера (Healthcheck)
|
||||||
|
# Docker будет периодически проверять статус контейнера. Это критично для Watchtower!
|
||||||
|
# Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый.
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"]
|
||||||
|
interval: 3m # Проверка каждые 3 минуты
|
||||||
|
timeout: 12s # Таймаут ответа - 12 секунды
|
||||||
|
start_period: 20s # Даем 20 секунд на стартап перед первой проверкой
|
||||||
|
retries: 3 # Unhealthy после 3 неудачных попыток
|
||||||
|
|
||||||
|
# 9. Логирование (Ротация)
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# 10. Ресурсы
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.50'
|
||||||
|
memory: 512M
|
||||||
|
mem_limit: 512m
|
||||||
|
|
||||||
|
# --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ ---
|
||||||
|
# Следит за реестром Gitea и обновляет контейнер web, если появился новый image
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: hypn0_watchtower
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
# Токен/Логин для вашего приватного реестра (нужно добавить в .env!)
|
||||||
|
# REPO_USER и REPO_PASS должны быть в .env файле на сервере
|
||||||
|
- REPO_USER=${REPO_USER}
|
||||||
|
- REPO_PASS=${REPO_PASS}
|
||||||
|
- WATCHTOWER_SCOPE=hypn0-scope
|
||||||
|
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
|
||||||
|
- DOCKER_API_VERSION=1.44
|
||||||
|
# Дополнительные опции для правильной работы с healthcheck
|
||||||
|
- WATCHTOWER_WAIT_ON_TIMEOUT=60 # Ждем 60 сек пока контейнер станет healthy перед финализацией
|
||||||
|
- WATCHTOWER_LIFECYCLE_HOOKS=true # Включаем lifecycle hooks для graceful shutdown
|
||||||
|
command: --interval 1800 --cleanup # Проверять каждые 30 минут
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
@@ -234,6 +234,9 @@ else:
|
|||||||
STORAGES['staticfiles'] = {
|
STORAGES['staticfiles'] = {
|
||||||
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
|
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
|
||||||
}
|
}
|
||||||
|
# Предотвращает 500 ошибку, если какой-то статический файл отсутствует в манифесте
|
||||||
|
WHITENOISE_MANIFEST_STRICT = env.bool('DJANGO_WHITENOISE_MANIFEST_STRICT', default=False)
|
||||||
|
|
||||||
# Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например,
|
# Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например,
|
||||||
# robots.txt, favicon.ico и т.п.)
|
# robots.txt, favicon.ico и т.п.)
|
||||||
WHITENOISE_ROOT = PUBLIC_DIR
|
WHITENOISE_ROOT = PUBLIC_DIR
|
||||||
|
|||||||
+18
-3
@@ -16,8 +16,9 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.sitemaps.views import sitemap
|
from django.contrib.sitemaps.views import sitemap
|
||||||
from django.urls import include, path
|
from django.urls import include, path, re_path
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from django.views.static import serve
|
||||||
|
|
||||||
from hypn0_site.sitemaps import sitemaps
|
from hypn0_site.sitemaps import sitemaps
|
||||||
from . import settings
|
from . import settings
|
||||||
@@ -30,10 +31,16 @@ urlpatterns = [
|
|||||||
path('', include('hypn0_site.urls')),
|
path('', include('hypn0_site.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
handler400 = "hypn0_site.views.error_400"
|
||||||
|
handler403 = "hypn0_site.views.error_403"
|
||||||
|
handler404 = "hypn0_site.views.error_404"
|
||||||
|
handler500 = "hypn0_site.views.error_500"
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
|
|
||||||
def _serve_public_root_file(request, path):
|
def _serve_public_root_file(request, path):
|
||||||
"""Отдаёт файлы из корня `public` в dev-режиме в utf-8."""
|
"""Отдаёт файлы из корня `public` в dev-режиме в utf-8."""
|
||||||
@@ -68,5 +75,13 @@ if settings.DEBUG:
|
|||||||
|
|
||||||
urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns
|
urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns
|
||||||
urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns]
|
urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns]
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.PUBLIC_DIR.joinpath('static'))
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
|
# ==============================================================================
|
||||||
|
# РАЗДАЧА МЕДИА-ФАЙЛОВ (/media/...)
|
||||||
|
# ВНИМАНИЕ: В боевом продакшене отдачей /media/ занимается внешний Nginx напрямую.
|
||||||
|
# Этот маршрут нужен для локального тестирования (в dev и в standalone prod-тестах без Nginx).
|
||||||
|
# ==============================================================================
|
||||||
|
# urlpatterns += [
|
||||||
|
# re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||||
|
# ]
|
||||||
|
|||||||
@@ -567,3 +567,19 @@ class TbBlogPost(models.Model):
|
|||||||
def get_absolute_url(self) -> str:
|
def get_absolute_url(self) -> str:
|
||||||
"""Возвращает канонический URL статьи блога."""
|
"""Возвращает канонический URL статьи блога."""
|
||||||
return f"/blog/{self.slug}" if self.slug else f"/blog/{self.pk}"
|
return f"/blog/{self.slug}" if self.slug else f"/blog/{self.pk}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blog_nav_title(self) -> str:
|
||||||
|
"""Детерминированный выбор названия раздела блога для навигации на основе ID статьи."""
|
||||||
|
nav_titles = (
|
||||||
|
"Блог Мозгоклюя",
|
||||||
|
"Записки Гипножабы",
|
||||||
|
"Дневник Мозговых Перцепторов",
|
||||||
|
"Хроники психо-резонанса",
|
||||||
|
"Мемуары астрального архива",
|
||||||
|
"Манифесты волновой матрицы",
|
||||||
|
"Бортовой журнал транс-погружения",
|
||||||
|
)
|
||||||
|
if not self.id:
|
||||||
|
return nav_titles[0]
|
||||||
|
return nav_titles[self.id % len(nav_titles)]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ HYPNO_ADJECTIVES = [
|
|||||||
"потусторонний", "спиральный", "зыбкий", "пахучий", "скользкий", "туманный", "онейрический", "эфирный",
|
"потусторонний", "спиральный", "зыбкий", "пахучий", "скользкий", "туманный", "онейрический", "эфирный",
|
||||||
"трансовый", "квантовый", "сочный", "гипно-абсурдный", "психоактивный", "сюрреалистичный", "магический",
|
"трансовый", "квантовый", "сочный", "гипно-абсурдный", "психоактивный", "сюрреалистичный", "магический",
|
||||||
"синтетический", "хроматический", "перцептивный", "психоделичный", "галлюцинаторный", "трансцендентальный",
|
"синтетический", "хроматический", "перцептивный", "психоделичный", "галлюцинаторный", "трансцендентальный",
|
||||||
"перфорированный",
|
"перфорированный", "перцепторный", "гипно-иллюзорный", "трансцендентальный", "вульгарный",
|
||||||
]
|
]
|
||||||
|
|
||||||
HYPNO_NOUNS = [
|
HYPNO_NOUNS = [
|
||||||
@@ -30,12 +30,14 @@ HYPNO_NOUNS = [
|
|||||||
"взмах", "взгляд", "морок", "туман", "свет", "кристалл", "поток", "сингулятор", "снег", "пульс", "глюк",
|
"взмах", "взгляд", "морок", "туман", "свет", "кристалл", "поток", "сингулятор", "снег", "пульс", "глюк",
|
||||||
"качан", "сигнал", "поток", "свет", "синтез", "рык", "трепет", "плюх", "Му", "бip-бip", "карачун", "фронтир",
|
"качан", "сигнал", "поток", "свет", "синтез", "рык", "трепет", "плюх", "Му", "бip-бip", "карачун", "фронтир",
|
||||||
"сторож", "привратник", "шёпот", "шлепок по жопе", "гриб", "плевок", "червяк", "пузырь", "психо-паук",
|
"сторож", "привратник", "шёпот", "шлепок по жопе", "гриб", "плевок", "червяк", "пузырь", "психо-паук",
|
||||||
|
"перцептор",
|
||||||
]
|
]
|
||||||
|
|
||||||
HYPNO_GENITIVES = [
|
HYPNO_GENITIVES = [
|
||||||
"сознания", "Гипножабы", "бессознательного", "пустоты", "матрицы", "психо-паутины", "слизня", "ноосферы",
|
"сознания", "Гипножабы", "бессознательного", "пустоты", "матрицы", "психо-паутины", "слизня", "ноосферы",
|
||||||
"нео-модерна", "транса", "глубины", "эфира", "плесени", "измерения", "пуантилизма", "субстанции",
|
"нео-модерна", "транса", "глубины", "эфира", "плесени", "измерения", "пуантилизма", "субстанции",
|
||||||
"неопределенности", "континуума", "кислоты", "сына подруги твоей мамы"
|
"неопределенности", "континуума", "кислоты", "сына подруги твоей мамы", "перцепции", "сновидений",
|
||||||
|
"галлюцинаций", "психоделики", "трансцендентности", "потустороннего мира",
|
||||||
]
|
]
|
||||||
|
|
||||||
HYPNO_POSTFIXES = [
|
HYPNO_POSTFIXES = [
|
||||||
|
|||||||
@@ -1386,16 +1386,31 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
|||||||
self.assertContains(resp_parent, "Глава 1: Первичная модуляция")
|
self.assertContains(resp_parent, "Глава 1: Первичная модуляция")
|
||||||
self.assertContains(resp_parent, "Глава 2: Фазовый сдвиг матрицы")
|
self.assertContains(resp_parent, "Глава 2: Фазовый сдвиг матрицы")
|
||||||
|
|
||||||
# 2. Проверка первой главы (связанный SVG, ссылка на родителя, следующая глава)
|
# 2. Проверка первой главы (связанный SVG, ссылка на родителя, следующая глава, хлебные крошки и JSON-LD)
|
||||||
resp_ch1 = self.client.get(f"/blog/{ch1.slug}")
|
resp_ch1 = self.client.get(f"/blog/{ch1.slug}")
|
||||||
self.assertEqual(resp_ch1.status_code, 200)
|
self.assertEqual(resp_ch1.status_code, 200)
|
||||||
self.assertContains(resp_ch1, "Глава 1: Первичная модуляция")
|
self.assertContains(resp_ch1, "Глава 1: Первичная модуляция")
|
||||||
self.assertContains(resp_ch1, "Материал входит в серию:")
|
self.assertContains(resp_ch1, "Материал входит в серию:")
|
||||||
self.assertContains(resp_ch1, "Связанная SVG-матрица")
|
self.assertContains(resp_ch1, self.item.s_title)
|
||||||
self.assertContains(resp_ch1, f"#{self.item.s_hash_id}")
|
self.assertContains(resp_ch1, f"#{self.item.s_hash_id}")
|
||||||
|
self.assertContains(resp_ch1, f"/gallery/{self.item.s_hash_id}")
|
||||||
|
self.assertContains(resp_ch1, "Скачать SVG")
|
||||||
|
self.assertContains(resp_ch1, "Вес:")
|
||||||
self.assertContains(resp_ch1, "Следующая глава цикла")
|
self.assertContains(resp_ch1, "Следующая глава цикла")
|
||||||
self.assertContains(resp_ch1, "Глава 2: Фазовый сдвиг матрицы")
|
self.assertContains(resp_ch1, "Глава 2: Фазовый сдвиг матрицы")
|
||||||
|
|
||||||
|
# Проверка хлебных крошек
|
||||||
|
self.assertContains(resp_ch1, 'aria-label="Хлебные крошки"')
|
||||||
|
self.assertContains(resp_ch1, ch1.blog_nav_title)
|
||||||
|
self.assertContains(resp_ch1, "Глава 1 из 2")
|
||||||
|
self.assertContains(resp_ch1, 'aria-current="page"')
|
||||||
|
|
||||||
|
# Проверка JSON-LD BreadcrumbList
|
||||||
|
self.assertContains(resp_ch1, '"@type": "BreadcrumbList"')
|
||||||
|
self.assertContains(resp_ch1, '"name": "Главная"')
|
||||||
|
self.assertContains(resp_ch1, '"name": "Хроники гипноза"')
|
||||||
|
self.assertContains(resp_ch1, series_parent.s_title)
|
||||||
|
|
||||||
# 3. Проверка второй главы (ссылка назад на главу 1)
|
# 3. Проверка второй главы (ссылка назад на главу 1)
|
||||||
resp_ch2 = self.client.get(f"/blog/{ch2.slug}")
|
resp_ch2 = self.client.get(f"/blog/{ch2.slug}")
|
||||||
self.assertEqual(resp_ch2.status_code, 200)
|
self.assertEqual(resp_ch2.status_code, 200)
|
||||||
@@ -1410,3 +1425,83 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
|||||||
# 5. Проверка 404 для несуществующего слага
|
# 5. Проверка 404 для несуществующего слага
|
||||||
resp_404 = self.client.get("/blog/non-existent-slug-xyz")
|
resp_404 = self.client.get("/blog/non-existent-slug-xyz")
|
||||||
self.assertEqual(resp_404.status_code, 404)
|
self.assertEqual(resp_404.status_code, 404)
|
||||||
|
|
||||||
|
@override_settings(DEBUG=False)
|
||||||
|
def test_custom_404_page_rendering(self):
|
||||||
|
"""Проверка отдачи кастомной страницы 404 в режиме DEBUG=False."""
|
||||||
|
response = self.client.get("/totally-non-existent-hypno-route-404")
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertContains(response, "Гипноузел не обнаружен", status_code=404)
|
||||||
|
self.assertContains(response, "СБОЙ МАТРИЦЫ // КОД? 404", status_code=404)
|
||||||
|
self.assertContains(response, "return-btn", status_code=404)
|
||||||
|
self.assertContains(response, "Стираем остатки несущес­твующей страницы", status_code=404)
|
||||||
|
|
||||||
|
@override_settings(DEBUG=True)
|
||||||
|
def test_debug_error_preview_routing(self):
|
||||||
|
"""Проверка dev-маршрутизации предпросмотра страниц ошибок при DEBUG=True."""
|
||||||
|
# 1. Индексный список страниц ошибок
|
||||||
|
resp_list = self.client.get("/_error/")
|
||||||
|
self.assertEqual(resp_list.status_code, 200)
|
||||||
|
self.assertContains(resp_list, "Центр отладки страниц ошибок")
|
||||||
|
self.assertContains(resp_list, "/_error/404")
|
||||||
|
self.assertContains(resp_list, "готов к просмотру")
|
||||||
|
|
||||||
|
# 2. Проверка каждого кода ошибки
|
||||||
|
expected_checks = [
|
||||||
|
("400", 400, "Сбой ментального протокола"),
|
||||||
|
("401", 401, "Требуется ментальная идентификация"),
|
||||||
|
("403", 403, "Отказано бюрократом 24-го уровня"),
|
||||||
|
("404", 404, "Гипноузел не обнаружен"),
|
||||||
|
("413", 413, "Перегрузка сенсорных каналов"),
|
||||||
|
("429", 429, "Слишком много мыслей в секунду"),
|
||||||
|
("500", 500, "Критический перегрев неокортекса"),
|
||||||
|
("502", 502, "Контейнер потерял сознание"),
|
||||||
|
("503", 503, "Плановый сеанс гипнотерапии"),
|
||||||
|
("504", 504, "Разрыв астральной связи"),
|
||||||
|
("under_reconstruction", 200, "Сектор на реконструкции"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for code, expected_status, text_fragment in expected_checks:
|
||||||
|
resp = self.client.get(f"/_error/{code}")
|
||||||
|
self.assertEqual(resp.status_code, expected_status, f"Ошибка при проверке кода {code}")
|
||||||
|
self.assertContains(resp, text_fragment, status_code=expected_status)
|
||||||
|
self.assertContains(resp, "return-btn", status_code=expected_status)
|
||||||
|
self.assertContains(resp, "logo-hypn0.svg", status_code=expected_status)
|
||||||
|
self.assertContains(resp, "thinking.svg", status_code=expected_status)
|
||||||
|
|
||||||
|
# 3. Предпросмотр с суффиксом .html
|
||||||
|
resp_404_html = self.client.get("/_error/404.html")
|
||||||
|
self.assertEqual(resp_404_html.status_code, 404)
|
||||||
|
self.assertContains(resp_404_html, "Гипноузел не обнаружен", status_code=404)
|
||||||
|
|
||||||
|
# 4. Несуществующий шаблон ошибки в dev-режиме
|
||||||
|
resp_non_existent = self.client.get("/_error/non-existent-code-999")
|
||||||
|
self.assertEqual(resp_non_existent.status_code, 404)
|
||||||
|
self.assertContains(resp_non_existent, "Шаблон для ошибки 'non-existent-code-999' не найден", status_code=404)
|
||||||
|
|
||||||
|
def test_custom_django_error_handlers(self):
|
||||||
|
"""Проверка прямого вызова стандартных Django-обработчиков ошибок."""
|
||||||
|
from django.test import RequestFactory
|
||||||
|
from hypn0_site.views import error_400, error_403, error_404, error_500
|
||||||
|
|
||||||
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
req400 = factory.get("/bad-request")
|
||||||
|
resp400 = error_400(req400)
|
||||||
|
self.assertEqual(resp400.status_code, 400)
|
||||||
|
self.assertIn("Сбой ментального протокола", resp400.content.decode("utf-8"))
|
||||||
|
|
||||||
|
req403 = factory.get("/forbidden")
|
||||||
|
resp403 = error_403(req403)
|
||||||
|
self.assertEqual(resp403.status_code, 403)
|
||||||
|
self.assertIn("Отказано бюрократом 24-го уровня", resp403.content.decode("utf-8"))
|
||||||
|
|
||||||
|
req404 = factory.get("/not-found")
|
||||||
|
resp404 = error_404(req404)
|
||||||
|
self.assertEqual(resp404.status_code, 404)
|
||||||
|
self.assertIn("Гипноузел не обнаружен", resp404.content.decode("utf-8"))
|
||||||
|
|
||||||
|
req500 = factory.get("/server-error")
|
||||||
|
resp500 = error_500(req500)
|
||||||
|
self.assertEqual(resp500.status_code, 500)
|
||||||
|
self.assertIn("Критический перегрев неокортекса", resp500.content.decode("utf-8"))
|
||||||
|
|||||||
@@ -19,4 +19,9 @@ urlpatterns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += [path("tmp/", views.tmp, name="web_tmp")]
|
urlpatterns += [
|
||||||
|
path("tmp/", views.tmp, name="web_tmp"),
|
||||||
|
path("_error", views.debug_error_preview, name="debug_error_list"),
|
||||||
|
path("_error/", views.debug_error_preview, name="debug_error_list_slash"),
|
||||||
|
path("_error/<path:code>", views.debug_error_preview, name="debug_error_preview"),
|
||||||
|
]
|
||||||
|
|||||||
@@ -157,6 +157,119 @@ def tmp(request: HttpRequest | None) -> HttpResponse:
|
|||||||
return render(request, "tmp.html", {})
|
return render(request, "tmp.html", {})
|
||||||
|
|
||||||
|
|
||||||
|
def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Dev-представление для отладки и предпросмотра страниц ошибок в режиме DEBUG.
|
||||||
|
Поддерживает пути вида /_error/404, /_error/404.html, /_error/500, /_error/, /_error/under_reconstruction.
|
||||||
|
"""
|
||||||
|
from django.template.exceptions import TemplateDoesNotExist
|
||||||
|
from django.template.loader import get_template
|
||||||
|
|
||||||
|
# Очистка имени шаблона от расширения .html и слэшей
|
||||||
|
raw_name = code.strip("/").removesuffix(".html") if code else ""
|
||||||
|
|
||||||
|
available_codes = [
|
||||||
|
"400",
|
||||||
|
"401",
|
||||||
|
"403",
|
||||||
|
"404",
|
||||||
|
"413",
|
||||||
|
"429",
|
||||||
|
"500",
|
||||||
|
"502",
|
||||||
|
"503",
|
||||||
|
"504",
|
||||||
|
"under_reconstruction",
|
||||||
|
]
|
||||||
|
|
||||||
|
if not raw_name:
|
||||||
|
# Индексная страница со списком всех кодов ошибок для dev-отладки
|
||||||
|
links = []
|
||||||
|
for c in available_codes:
|
||||||
|
exists = False
|
||||||
|
for tpl in [f"_error/{c}.html", f"{c}.html"]:
|
||||||
|
try:
|
||||||
|
get_template(tpl)
|
||||||
|
exists = True
|
||||||
|
break
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
pass
|
||||||
|
status_mark = "готов к просмотру" if exists else "шаблон не создан"
|
||||||
|
links.append(f"<li><a href='/_error/{c}'><strong>/{c}</strong></a> ({status_mark})</li>")
|
||||||
|
|
||||||
|
html = f"""<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Dev: Предпросмотр страниц ошибок // HypnoSVG</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: ui-monospace, monospace; background: #09090b; color: #d9f99d; padding: 2rem; line-height: 1.6; }}
|
||||||
|
a {{ color: #a3e635; text-decoration: none; border-bottom: 1px dotted #a3e635; }}
|
||||||
|
a:hover {{ border-bottom-style: solid; }}
|
||||||
|
ul {{ list-style-type: square; margin-top: 1rem; }}
|
||||||
|
li {{ margin-bottom: 0.5rem; }}
|
||||||
|
.card {{ background: #18181b; border: 1px solid #27272a; border-radius: 8px; padding: 1.5rem; max-width: 600px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Центр отладки страниц ошибок (DEBUG=True)</h2>
|
||||||
|
<p>Выберите код для инспекции верстки и интерактивных скриптов перекалибровки:</p>
|
||||||
|
<ul>
|
||||||
|
{''.join(links)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
return HttpResponse(html, content_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
# Поиск соответствующего шаблона
|
||||||
|
template_candidates = [
|
||||||
|
f"_error/{raw_name}.html",
|
||||||
|
f"{raw_name}.html",
|
||||||
|
f"_error/{raw_name}",
|
||||||
|
raw_name,
|
||||||
|
]
|
||||||
|
|
||||||
|
found_template = None
|
||||||
|
for candidate in template_candidates:
|
||||||
|
try:
|
||||||
|
get_template(candidate)
|
||||||
|
found_template = candidate
|
||||||
|
break
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not found_template:
|
||||||
|
return HttpResponse(
|
||||||
|
f"<h1>Шаблон для ошибки '{raw_name}' не найден</h1><p>Проверены: {', '.join(template_candidates)}</p><p><a href='/_error/'>← Вернуться к списку</a></p>",
|
||||||
|
status=404,
|
||||||
|
content_type="text/html; charset=utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
status_code = 200
|
||||||
|
if raw_name.isdigit() and 400 <= int(raw_name) <= 599:
|
||||||
|
status_code = int(raw_name)
|
||||||
|
|
||||||
|
return render(request, found_template, {}, status=status_code)
|
||||||
|
|
||||||
|
|
||||||
|
def error_400(request: HttpRequest, exception: Exception | None = None) -> HttpResponse:
|
||||||
|
return render(request, "_error/400.html", {}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def error_403(request: HttpRequest, exception: Exception | None = None) -> HttpResponse:
|
||||||
|
return render(request, "_error/403.html", {}, status=403)
|
||||||
|
|
||||||
|
|
||||||
|
def error_404(request: HttpRequest, exception: Exception | None = None) -> HttpResponse:
|
||||||
|
return render(request, "_error/404.html", {}, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
def error_500(request: HttpRequest) -> HttpResponse:
|
||||||
|
return render(request, "_error/500.html", {}, status=500)
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def generate(request: HttpRequest) -> HttpResponse:
|
def generate(request: HttpRequest) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
@@ -710,6 +823,7 @@ def blog_detail(request: HttpRequest, slug: str) -> HttpResponse:
|
|||||||
"svg_stats": svg_stats,
|
"svg_stats": svg_stats,
|
||||||
"series_parent": series_parent,
|
"series_parent": series_parent,
|
||||||
"series_posts": series_posts,
|
"series_posts": series_posts,
|
||||||
|
"series_total_count": len(series_posts) if series_posts else 0,
|
||||||
"prev_post": prev_post,
|
"prev_post": prev_post,
|
||||||
"next_post": next_post,
|
"next_post": next_post,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,13 +101,16 @@ val=>{if(val)document.documentElement.classList.add('dark');else document.docume
|
|||||||
файла css/tailwind-custom.css (подключён в input.css через @import).{% endcomment %}
|
файла css/tailwind-custom.css (подключён в input.css через @import).{% endcomment %}
|
||||||
<link rel="stylesheet" href="{% static 'css/tailwind.min.css' %}" />{% endif %}
|
<link rel="stylesheet" href="{% static 'css/tailwind.min.css' %}" />{% endif %}
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
[
|
||||||
"@context": "https://schema.org",
|
{
|
||||||
"@type": "WebSite",
|
"@context": "https://schema.org",
|
||||||
"name": "hypn0.xyz",
|
"@type": "WebSite",
|
||||||
"url": "{{ request.scheme }}://{{ request.get_host }}/"
|
"name": "hypn0.xyz",
|
||||||
}{% block EXTRA_LD_JSON %}
|
"url": "{{ request.scheme }}://{{ request.get_host }}/"
|
||||||
{% endblock EXTRA_LD_JSON %}</script>
|
}{% block EXTRA_LD_JSON %}
|
||||||
|
{% endblock EXTRA_LD_JSON %}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
||||||
<script src="{% static 'js/htmx.min.js' %}" defer></script>
|
<script src="{% static 'js/htmx.min.js' %}" defer></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>400 — Сбой ментального протокола | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 400: Некорректный запрос к матрице HypnoSVG. Сервер не распознал структуру переданного сигнала." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="400 Bad Request Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">СБОЙ МАТРИЦЫ // КОД 400</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Сбой перцепционного протокола</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Клиент искажает ментальные сигналы или имеет невалидную структуру биоритмов мозговых данных. Неокортекс сервера отверг запрос из-за синтаксического диссонанса.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Некорректно сформулированная мысль разрушает гармонию халфтона. Перепроверьте параметры запроса перед повторным сеансом внушения.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Сбросить параметры и вернуться</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
|
||||||
|
<span>← На шаг назад</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация очистки буфера…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 400 BAD REQUEST █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Декодируем искажённые байты входящего сигнала…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Бюрократ 24-го уровня отклоняет невалидный пакет…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Очищаем буфер неокортекса от ментального шума…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Протокол сброшен! Возвращаемся на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>401 — Требуется ментальная идентификация | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 401: Доступ к закрытому узлу HypnoSVG требует прохождения авторизации." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="401 Unauthorized Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">ОГРАНИЧЕНИЕ ДОСТУПА // КОД 401</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Требуется ментальная идентификация</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Вы попытались проникнуть в закрытый сектор подсознания без действующего пропуска или аутентификационного чипа.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Свободная ментальность и анонимные мысли не допускаются к генераторам высшего порядка. Предъявите биометрический слепок или вернитесь в общий сектор.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Вернуться в общий сектор</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
|
||||||
|
<span>← На шаг назад</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация сканирования биометрии…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 401 UNAUTHORIZED █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Сканируем ментальные отпечатки и токены доступа…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Проверяем наличие чипа мозгового слизня…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня фиксирует отсутствие пропуска…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Идентификация отклонена! Телепор­тация в безопасную зону…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>403 — Отказано бюрократом 24-го уровня | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 403: Доступ к ресурсу категорически запрещен протоколами безопасности HypnoSVG." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="403 Forbidden Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">ДОСТУП ЗАБЛОКИРОВАН // КОД 403</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Ментальная блокировка</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
У вас нет достаточного уровня перцепционного допуска для просмотра этого сектора подсознания. Вердикт окончательный и обжалованию не подлежит.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Форма 38-Б о праве на любопытство не была заверена в Центральном Бюрократическом Управлении. Несанкционированное созерцание карается понижением до 34-го уровня.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Принять запрет и вернуться</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
|
||||||
|
<span>← На шаг назад</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация проверки гербовых печатей…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 403 FORBIDDEN █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Проверяем гербовые печати и визы Центрального Бюро…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Выписываем штраф за несанкционированное любопытство…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Блокируем блоки краткосрочной памяти и транспорт в гипокамп…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Протокол закрыт! Перенаправ­ление на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>404 — Векторная реальность не найдена | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 404: Запрашиваемая страница HypnoSVG не существует или стёрта волей Гипножабы." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="404 Thinking Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">СБОЙ МАТРИЦЫ // КОД? 404</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Гипноузел не обнаружен</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Запра­шиваемый блок ментального пространства стёрт из подсознания сервера или никогда не существовал в этой версии вселенной.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Попытка осознать отсутс­твующую координату приводит к локальному перегреву неокортекса. Не вгляды­вайтесь в пустоту. Гипножаба рекомендует немедленно сменить вектор внимания.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Подчиниться и вернуться на главную</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
|
||||||
|
<span>← На шаг назад</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация перека­либровки сознания…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 404 NOT FOUND █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Стираем остатки несущес­твующей страницы из подсознания…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Бюрократ 24-го уровня заверяет протокол 404…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Синхронизируем частоту мерцания с перцеп­торным полем…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Внушение принято! Телепор­тация на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000; // 7 секунд, чтобы комфортно прочитать все фазы внушения
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>413 — Перегрузка сенсорных каналов | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 413: Загружаемый файл превышает допустимый размер ментального буфера HypnoSVG." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="413 Payload Too Large Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">ПРЕВЫШЕН ЛИМИТ // КОД 413</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Перегрузка сенсорных каналов</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Загружаемое изображение оказалось слишком массивным для восприятия ментальной матрицей. Сервер перцепций отказался поглощать такой объём байтов.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Попытка скормить гигабайты растра хрупкому SVG-генератору расценивается как диверсия. Уменьшите разрешение или сожмите файл перед повторной загрузкой в ментальную матрицу.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Сжать восприятие и вернуться</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
|
||||||
|
<span>← На шаг назад</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация сброса переполненного буфера…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 413 PAYLOAD TOO LARGE █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Сбрасываем перегруженный ментальный буфер входного потока…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Охлаждаем приемные каналы Nginx жидким азотом…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня успокаивает перцепционные волны ментальности…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Буфер чист! Возвращаемся к генератору…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>429 — Слишком много мыслей в секунду | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 429: Превышен лимит запросов к генератору HypnoSVG. Сделайте ментальную паузу." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="429 Too Many Requests Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">ЗАЩИТА ОТ СПАМА // КОД 429</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Слишком много мыслей в секунду</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Вы отправляете импульсы быстрее, чем Гипножаба успевает перемигиваться. Включена защита от ментального спама и перегрева процессоров.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Перцепционный генератор требует расслабленного состояния мышления, а не ментальных судорог и урагана кликов. Сделайте глубокий вдох, сосчитайте до сорока двух и позвольте трансу стабилизироваться.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Остудить пыл и вернуться</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
|
||||||
|
<span>← На шаг назад</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация медитативного охлаждения…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 429 TOO MANY REQUESTS █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Замедляем частоту синаптических импульсов…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Восстанавливаем квоту запросов в реестре Гипножабы…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Приводим сознание в состояние медитативного покоя…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Лимиты восстановлены! Возвращаемся на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>500 — Критический перегрев неокортекса | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 500: Внутренний сбой сервера HypnoSVG. Ведутся ментальные восстановительные работы." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="500 Internal Server Error Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">АВАРИЯ СЕРВЕРА // КОД 500</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Нейроклинч интроспекций</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Внутренний обработчик Т-волн подсознания столкнулся с непредвиденным логическим парадоксом. Ментальные службы уже приведены в состояние боевого транса.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Стек вызовов испустил синий дым, но паника строго запрещена циркуляром №16. Гипножаба, Мозгоклюй и дежурные Бри-Гады мозговых слизней уже инспектируют упавший поток перцепции и устраняет ментальную деградацию.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Дождаться регенерации и вернуться</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.location.reload()">
|
||||||
|
<span>↻ Повторить попытку</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация аварийной регенерации ядра…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 500 INTERNAL SERVER ERROR █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Локализуем утечку астральной памяти в ядре сервера…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Бюрократ 24-го уровня перезапускает упавший процесс…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Гипножаба восстанавливает гармонию матриц неокотекса…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Ядро стабилизировано! Телепор­тация на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>502 — Контейнер потерял сознание | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 502: Прокси-сервер не получил ответа от бэкенда HypnoSVG." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="502 Bad Gateway Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">ОБРЫВ ШЛЮЗА // КОД 502</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Нет связи с Гипокампом</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Прокси-сервер Nginx послал импульс через энторинальную кору в Django-гипокамп, но обратный синапсический отклик потерян. Аксоны в состоянии звенящей астральной пустоты.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Gunicorn ушёл в глубокую интроспекцию или временно дезориентирован в docker-пространстве. Бри-Гады мозговых слизней уже инспектируют упавший поток перцепции и устраняет ментальную деградацию.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Восстановить связь и вернуться</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.location.reload()">
|
||||||
|
<span>↻ Перезагрузить страницу</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация реанимации шлюза…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 502 BAD GATEWAY █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Пингуем упавший контейнер интроспекций через Unix-сокеты…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Подаем разряд бodрящих волн Шумана в воркеры Gunicorn…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Прокси-шлюз Nginx восстанавливает ментальный маршрут…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Мост налажен! Возвращаемся на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>503 — Плановый сеанс гипнотерапии | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 503: Сервис HypnoSVG временно недоступен из-за технического обслуживания или пиковой нагрузки." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="503 Service Unavailable Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">ТЕХОБСЛУЖИВАНИЕ // КОД 503</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Плановый сеанс гипнотерапии</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Сервер временно закрыт на калибровку ментальных матриц или испытывает запредельный наплыв адептов Гипножабы.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Проводятся регламентные работы по смазке осей гипнотических нейроспиралей и продувке камер интроспекций халфтона. Задействованы усиленные технические Бри-Гады мозговых слизней под надзором Мозгоклюя. Скоро сервис вернётся в строй.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Дождаться окончания сеанса</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.location.reload()">
|
||||||
|
<span>↻ Проверить статус</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация технической диагностики…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 503 SERVICE UNAVAILABLE █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Калибруем точность алгоритмов смыслового мерцания…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Смазываем оси гипнотических спиралей…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня завершает технический аудит…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Профилактика завершена без подписания акта о выполнении работ! Телепор­тация на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>504 — Разрыв астральной связи | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Ошибка 504: Превышено время ожидания ответа от вышестоящего сервера HypnoSVG." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="504 Gateway Timeout Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">ТАЙМАУТ ШЛЮЗА // КОД 504</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Разрыв астральной связи</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Вышестоящий перцептивный узел в состоянии рекурсивной интроспекции и не успевает ответить в отведённый квант времени.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Время — понятие относительное, но таймаут Nginx абсолютен. Ментальный транс осознанных сновидений думателя прерван во избежание <nobr>зацикливания нейро-континуума.</nobr></em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Восстановить таймлайн и вернуться</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.location.reload()">
|
||||||
|
<span>↻ Повторить запрос</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация синхронизации таймлайна…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ ERROR 504 GATEWAY TIMEOUT █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Прерываем бесконечные квантовые вычисления…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Перезапускаем таймеры ожидания шлюза Nginx…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Синхронизируем ментальное состояние с сервером Гипножабы…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Связь восстановлена! Возвращаемся на главную…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Реконструкция матрицы | HypnoSVG</title>
|
||||||
|
<meta name="description" content="Сектор временно закрыт на квантовую реконструкцию и модернизацию алгоритмов HypnoSVG." />
|
||||||
|
<meta name="robots" content="noindex, follow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('darkMode');
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (saved === 'true' || (saved === null && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #fafaf9;
|
||||||
|
--bg-header: rgba(243, 244, 246, 0.85);
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-main: #047857;
|
||||||
|
--text-muted: #4b5563;
|
||||||
|
--text-dim: #9ca3af;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--accent-color: #059669;
|
||||||
|
--accent-bg: #ecfdf5;
|
||||||
|
--btn-bg: #059669;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
--btn-hover: #047857;
|
||||||
|
--progress-bg: #e5e7eb;
|
||||||
|
--progress-fill: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg-color: #09090b;
|
||||||
|
--bg-header: rgba(30, 41, 59, 0.75);
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--text-main: #d9f99d;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
--border-color: #27272a;
|
||||||
|
--accent-color: #a3e635;
|
||||||
|
--accent-bg: #14280f;
|
||||||
|
--btn-bg: #a3e635;
|
||||||
|
--btn-text: #09090b;
|
||||||
|
--btn-hover: #bef264;
|
||||||
|
--progress-bg: #27272a;
|
||||||
|
--progress-fill: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: var(--bg-header);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2.5rem 1.75rem;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art {
|
||||||
|
width: min(300px, 100%);
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: spin-spiral 40s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-spiral {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.error-art img, .error-art svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hypno-quote {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.875rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: min(16rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--btn-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Задержка и смешные сообщения перекалибровки */
|
||||||
|
.calibration-panel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px dashed var(--accent-color);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--progress-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-fill);
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-subtext {
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-grid {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo-link" title="HypnoSVG (hypn0) — На главную">
|
||||||
|
<img src="/static/img/logo-hypn0.svg" onerror="this.onerror=null; this.src='/media/_error/logo-hypn0.svg';" alt="HypnoSVG" class="logo-img" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="theme-toggle" id="theme-btn" aria-label="Переключить тему" title="Переключить тему">
|
||||||
|
<span id="theme-icon">●</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-art">
|
||||||
|
<img src="/static/img/thinking.svg" onerror="this.onerror=null; this.src='/media/_error/thinking.svg';" alt="Under Reconstruction Slug" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-code-badge">РЕКОНСТРУКЦИЯ ИНТРОСПЕКЦИИ</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">Отдел мозга на реконструкции</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Узел ментальной матрицы перестраивается по бредо-чертежам Гипножабы. Бри-Гады мозговых слизней под контролем Мозгоклюя разворачивают новый слой интроспекций. Скоро здесь появится нечто гипнотически непознаваемое.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hypno-quote">
|
||||||
|
<big>Справка бюрократа 24-го уровня:</big><br/> <br/>
|
||||||
|
<span style="margin-left: -0.49em;">«</span><em>Ведутся строительно-монтажные работы по укладке новых векторных силовых линий. Проход без мантальной каски и без вживленного мозгового слизня ограничен даже гражданам в глубоком трансе.</em>»
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-box">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary" id="return-btn">
|
||||||
|
<span>● Попытка вынырнуть из интроспекции</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
|
||||||
|
<span>← На шаг назад</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель калибровки с задержкой и смешными фразами -->
|
||||||
|
<div class="calibration-panel" id="calibration-panel">
|
||||||
|
<div class="calibration-status" id="calibration-status">
|
||||||
|
Инициа­лизация перемещения из зоны работ…
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="calibration-subtext">
|
||||||
|
<span>Снижение ментальной нагрузки на кластер</span>
|
||||||
|
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="ascii-grid">░▒▓█ UNDER RECONSTRUCTION █▓▒░</div>
|
||||||
|
<div>Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. Управление темой оформления (Light / Dark)
|
||||||
|
var themeBtn = document.getElementById('theme-btn');
|
||||||
|
var themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
|
function updateThemeIcon() {
|
||||||
|
var isDark = document.documentElement.classList.contains('dark');
|
||||||
|
themeIcon.textContent = isDark ? '●' : '○';
|
||||||
|
}
|
||||||
|
updateThemeIcon();
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
var isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('darkMode', isDark);
|
||||||
|
updateThemeIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Умная кнопка с задержкой и смешными сообщениями перекалибровки
|
||||||
|
var returnBtn = document.getElementById('return-btn');
|
||||||
|
var calibPanel = document.getElementById('calibration-panel');
|
||||||
|
var calibStatus = document.getElementById('calibration-status');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
var isCalibrating = false;
|
||||||
|
|
||||||
|
var phrases = [
|
||||||
|
{ at: 0, text: "✦ Фаза 1/4: Закладываем квантовый фундамент новой функциональности…" },
|
||||||
|
{ at: 25, text: "✦ Фаза 2/4: Монтируем передовые генераторы векторных паттернов…" },
|
||||||
|
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня согласует счет-фактуру…" },
|
||||||
|
{ at: 85, text: "✦ Фаза 4/4: Акт согласования готов! Это не поможет, но возвращаемся в активную зону сайта…" }
|
||||||
|
];
|
||||||
|
|
||||||
|
returnBtn.addEventListener('click', function() {
|
||||||
|
if (isCalibrating) return;
|
||||||
|
isCalibrating = true;
|
||||||
|
|
||||||
|
returnBtn.disabled = true;
|
||||||
|
returnBtn.style.opacity = '0.7';
|
||||||
|
returnBtn.innerHTML = '<span>Выполняется внушение…</span>';
|
||||||
|
calibPanel.style.display = 'block';
|
||||||
|
|
||||||
|
var duration = 12000;
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
var elapsed = Date.now() - startTime;
|
||||||
|
var progress = Math.min(100, Math.round((elapsed / duration) * 100));
|
||||||
|
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
|
||||||
|
// Подбираем актуальную смешную фразу
|
||||||
|
for (var i = phrases.length - 1; i >= 0; i--) {
|
||||||
|
if (progress >= phrases[i].at) {
|
||||||
|
calibStatus.innerHTML = phrases[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
и <abbr title="General Data Protection Regulation">GDPR</abbr> и протоколами Нового Нью-Йорка,
|
и <abbr title="General Data Protection Regulation">GDPR</abbr> и протоколами Нового Нью-Йорка,
|
||||||
этот сайт использует файлы-присоски (cookies). ВСЕ СЛАВЯТ ГИПНОЖАБУ. Ваши обезли­ченные данные
|
этот сайт использует файлы-присоски (cookies). ВСЕ СЛАВЯТ ГИПНОЖАБУ. Ваши обезли­ченные данные
|
||||||
считываются алгоритмами ментального мерцания. Оставаясь на гипно-платформе <strong>hypn0</strong>, вы добровольно
|
считываются алгоритмами ментального мерцания. Оставаясь на гипно-платформе <strong>hypn0</strong>, вы добровольно
|
||||||
впадаете в транс и принимаете условия нашей <a href="/info/privacy-policy">политики
|
впадаете в транс и принимаете условия нашей <a href="/blog/security-and-privacy-policy">политики
|
||||||
коконфи­денциаль­ности</a>. <strong>Не пытайтесь переключить вкладку!</strong>
|
конфи­денциаль­ности</a>. <strong>Не пытайтесь переключить вкладку!</strong>
|
||||||
</p>
|
</p>
|
||||||
<button @click="const vid = crypto.randomUUID(); localStorage.setItem('hypn0_vid', vid); document.cookie = `hypn0_vid=${vid}; path=/; max-age=63072000; SameSite=Lax`; agreed = true" class="text-lg px-6 py-2 my-6 border-2 rounded-md bg-emerald-600 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 text-white">Подчиниться Гипножабе!</button>
|
<button @click="const vid = crypto.randomUUID(); localStorage.setItem('hypn0_vid', vid); document.cookie = `hypn0_vid=${vid}; path=/; max-age=63072000; SameSite=Lax`; agreed = true" class="text-lg px-6 py-2 my-6 border-2 rounded-md bg-emerald-600 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 text-white">Подчиниться Гипножабе!</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block PAGE_TITLE %}{{ post.s_title|striptags }} | Хроники гипноза HypnoSVG{% endblock %}
|
||||||
|
|
||||||
|
{% block META_TITLE %}{{ post.s_title|striptags }} — HypnoSVG{% endblock %}
|
||||||
|
|
||||||
|
{% block META_DESCRIPTION %}{% if post.s_teaser %}{{ post.s_teaser|striptags|truncatechars:160 }}{% else %}Статья из хроник гипноза HypnoSVG: {{ post.s_title|striptags }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block OG_TITLE %}{{ post.s_title|striptags }} | HypnoSVG{% endblock %}
|
||||||
|
|
||||||
|
{% block OG_DESCRIPTION %}{% if post.s_teaser %}{{ post.s_teaser|striptags|truncatechars:200 }}{% else %}Статья из хроник гипноза HypnoSVG: {{ post.s_title|striptags }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block OG_IMAGE %}{% if post.f_cover_img %}{{ request.scheme }}://{{ request.get_host }}{{ post.f_cover_img.url }}{% elif post.k_item and post.k_item.file_svg %}{{ request.scheme }}://{{ request.get_host }}{{ post.k_item.file_svg.url }}{% else %}{% static 'img/og-hypn0-default.png' %}{% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block CANONICAL %}{{ request.scheme }}://{{ request.get_host }}{{ post.get_absolute_url }}{% endblock %}
|
||||||
|
|
||||||
|
{% block EXTRA_LD_JSON %},
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 1,
|
||||||
|
"name": "Главная",
|
||||||
|
"item": "{{ request.scheme }}://{{ request.get_host }}{% url 'hypn0_site:index' %}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 2,
|
||||||
|
"name": "Хроники гипноза",
|
||||||
|
"item": "{{ request.scheme }}://{{ request.get_host }}{% url 'hypn0_site:blog_feed' %}"
|
||||||
|
}{% if series_parent %},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 3,
|
||||||
|
"name": "{{ series_parent.s_title|striptags|escapejs }}",
|
||||||
|
"item": "{{ request.scheme }}://{{ request.get_host }}{{ series_parent.get_absolute_url }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 4,
|
||||||
|
"name": "{{ post.s_title|striptags|escapejs }}",
|
||||||
|
"item": "{{ request.scheme }}://{{ request.get_host }}{{ post.get_absolute_url }}"
|
||||||
|
}{% else %},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 3,
|
||||||
|
"name": "{{ post.s_title|striptags|escapejs }}",
|
||||||
|
"item": "{{ request.scheme }}://{{ request.get_host }}{{ post.get_absolute_url }}"
|
||||||
|
}{% endif %}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
"headline": "{{ post.s_title|striptags|escapejs }}",
|
||||||
|
"description": "{{ post.s_teaser|striptags|escapejs }}",
|
||||||
|
"datePublished": "{{ post.d_published_at|date:'c' }}",
|
||||||
|
"dateModified": "{{ post.d_updated_at|date:'c' }}",
|
||||||
|
"mainEntityOfPage": {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "{{ request.scheme }}://{{ request.get_host }}{{ post.get_absolute_url }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endblock EXTRA_LD_JSON %}
|
||||||
|
|
||||||
|
{% block CONTENT %}
|
||||||
|
<article class="mx-auto space-y-10">
|
||||||
|
|
||||||
|
<!-- Навигационная полоса / Хлебные крошки -->
|
||||||
|
<nav aria-label="Хлебные крошки" class="flex flex-wrap items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-stone-500 dark:text-stone-400">
|
||||||
|
<a href="{% url 'hypn0_site:index' %}" class="inline-flex items-center gap-1 " title="Главная">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||||
|
</svg>
|
||||||
|
<span class="hidden sm:inline">Главная</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="text-stone-300 dark:text-zinc-700">/</span>
|
||||||
|
|
||||||
|
<a href="{% url 'hypn0_site:blog_feed' %}">
|
||||||
|
{{ post.blog_nav_title }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if series_parent %}
|
||||||
|
<span class="text-stone-300 dark:text-zinc-700">/</span>
|
||||||
|
<a href="{{ series_parent.get_absolute_url }}" title="{{ series_parent.s_title|striptags }}">
|
||||||
|
Глава {{ post.i_order }}{% if series_total_count %} из {{ series_total_count }}{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="text-stone-300 dark:text-zinc-700">/</span>
|
||||||
|
|
||||||
|
<span class="text-stone-400 dark:text-stone-500 truncate max-w-[180px] sm:max-w-xs md:max-w-sm font-normal" aria-current="page" title="{{ post.s_title|striptags }}">
|
||||||
|
{{ post.s_title|safe }}
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Серия: индикатор цикла (если пост является частью серии) -->
|
||||||
|
{% if series_parent %}
|
||||||
|
<div class="bg-purple-500/10 dark:bg-purple-950/30 border border-purple-500/20 rounded-2xl p-4 sm:p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-purple-500 animate-pulse shrink-0"></span>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="text-stone-500 dark:text-stone-400">Материал входит в серию:</span>
|
||||||
|
<a href="{{ series_parent.get_absolute_url }}" class="font-bold text-purple-700 dark:text-purple-300 hover:underline border-0 ml-1">
|
||||||
|
{{ series_parent.s_title|safe }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] text-purple-600 dark:text-purple-400 shrink-0">
|
||||||
|
Глава {{ post.i_order }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- ЕДИНАЯ ДВУХКОЛОНОЧНАЯ СЕТКА: 1/4 слева (превью + SVG), 3/4 справа (заголовок + дата + тизер + контент) -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-x-6 items-start">
|
||||||
|
|
||||||
|
<!-- 1. Верхний ряд слева (1/4): Тизер-превью обложки -->
|
||||||
|
<div class="md:col-span-1 w-full pb-8">
|
||||||
|
{% if post.f_cover_img %}{# ЕСТЬ ТИЗЕР-КАРТИНКА И ПОКАЗЫВАЕМ ЕË #}
|
||||||
|
<div class="aspect-square sm:aspect-[4/3] md:aspect-square rounded-2xl overflow-hidden border border-stone-200 dark:border-zinc-800 shadow-md bg-stone-100 dark:bg-zinc-900">
|
||||||
|
<img src="{{ post.f_cover_img.url }}"
|
||||||
|
alt="{{ post.s_title|striptags }}"
|
||||||
|
class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
{% else %}{# НЕТ ТИЗЕР-КАРТИНКИ И ПОКАЗЫВАЕМ ЗАГЛУШКУ #}
|
||||||
|
<div class="aspect-square sm:aspect-[4/3] md:aspect-square rounded-2xl border border-dashed border-stone-300 dark:border-zinc-700 bg-stone-100/40 dark:bg-zinc-900/40 flex flex-col items-center justify-center p-3 text-stone-400 dark:text-stone-600 text-center">
|
||||||
|
<img src="{% static 'img/brain-slug.svg' %}" alt="Гипножаба" class="w-10 h-10 opacity-40 mb-1">{# <-- СЮДА ДЕФОЛТНУЮ КАРТИНКУ #}
|
||||||
|
<span class="text-[10px] uppercase tracking-wider">Тут ничего не нарисовано</span>{# <-- А ПОСЛЕ ЭТО УБРАТЬ #}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. Верхний ряд справа (3/4): Заголовок + Дата + Тизер с непрерывной пунктирной линией -->
|
||||||
|
<div class="md:col-span-3 border-l-1 border-purple-500 border-dotted pl-6 pb-8 space-y-4">
|
||||||
|
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-black tracking-tight text-stone-900 dark:text-stone-100 leading-tight">
|
||||||
|
{{ post.s_title|safe }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Дата и время публикации статьи в рамочке с иконкой часов -->
|
||||||
|
<div class="flex items-center justify-start">
|
||||||
|
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-purple-500/10 text-purple-700 dark:text-purple-300 border border-purple-500/20 font-bold text-sm">
|
||||||
|
<svg class="w-3.5 h-3.5 text-purple-700 dark:text-purple-300 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
<time datetime="{{ post.d_published_at|date:'c' }}">
|
||||||
|
{{ post.d_published_at|date:'d E 30y, H:i:s' }}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Лид / Тизер статьи -->
|
||||||
|
{% if post.s_teaser %}
|
||||||
|
<div class="text-base sm:text-lg text-stone-600 dark:text-stone-300 leading-relaxed font-sans p-4 sm:p-5 bg-stone-100 dark:bg-zinc-900 border border-stone-200 dark:border-zinc-800 rounded-2xl">
|
||||||
|
{{ post.s_teaser|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Нижний ряд слева (1/4): Связанный SVG + Музейная экспликация (выровнены по вертикали с первой строкой контента) -->
|
||||||
|
<div class="md:col-span-1 space-y-4">
|
||||||
|
{% if post.k_item %}
|
||||||
|
<aside class="space-y-3 md:sticky md:top-6">
|
||||||
|
<!-- Интерактивное SVG-полотно экспоната -->
|
||||||
|
<div class="aspect-square rounded-2xl overflow-hidden bg-stone-950 shadow-md flex items-center justify-center p-2 select-none hypn0-detail-canvas [&>svg]:w-full [&>svg]:h-full [&>svg]:max-w-full [&>svg]:max-h-full [&>svg]:object-contain">
|
||||||
|
{% if active_svg %}
|
||||||
|
<div class="w-full h-full flex items-center justify-center pointer-events-auto">
|
||||||
|
<template shadowrootmode="open">
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
--hypn0-play: running !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ active_svg|safe }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
{% elif post.k_item.file_svg %}
|
||||||
|
<img src="{{ post.k_item.file_svg.url }}" alt="{{ post.k_item.s_title }}" class="max-w-full max-h-full object-contain">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Музейная экспликация (Табличка с описанием экспоната) -->
|
||||||
|
<div class="bg-stone-100/80 dark:bg-zinc-900/80 border border-stone-200/80 dark:border-zinc-800 rounded-2xl p-3.5 space-y-2.5 text-xs shadow-sm">
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="font-bold text-stone-900 dark:text-stone-100 text-sm leading-snug">
|
||||||
|
{{ post.k_item.s_title|safe }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-1.5 my-2.5">
|
||||||
|
<a href="{% url 'hypn0_site:gallery_detail' post.k_item.s_hash_id %}"
|
||||||
|
class="text-[10px]"
|
||||||
|
title="Открыть картину в галерее">#{{ post.k_item.s_hash_id }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-1.5 mt-1.5">
|
||||||
|
<span class="text-[10px] px-2 py-0.5 rounded-full bg-purple-500/10 text-purple-700 dark:text-purple-300 border border-purple-500/20 font-bold">
|
||||||
|
{{ post.k_item.get_i_level_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-stone-200/60 dark:border-zinc-800 pt-2 space-y-1.5 text-[11px] text-stone-600 dark:text-stone-400">
|
||||||
|
{% if svg_stats.total_oscillators %}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="opacity-70">Осцилляций:</span>
|
||||||
|
<span class="font-bold text-stone-900 dark:text-stone-200">{{ svg_stats.total_oscillators }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if svg_stats.viewbox %}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="opacity-70">viewBox:</span>
|
||||||
|
<span class="font-mono text-purple-600 dark:text-purple-400">{{ svg_stats.viewbox }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if svg_stats.total_kb %}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="opacity-70">Вес:</span>
|
||||||
|
<span class="font-mono text-stone-900 dark:text-stone-200">{{ svg_stats.total_kb }} Кб</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span><svg class="w-3.5 h-3.5 text-emerald-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||||
|
</svg></span>
|
||||||
|
<span class="inline-flex items-center gap-1 font-bold text-stone-900 dark:text-stone-200">
|
||||||
|
{{ post.k_item.i_views_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span><svg class="w-3.5 h-3.5 fill-rose-500 text-rose-500 shrink-0" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
|
||||||
|
</svg></span>
|
||||||
|
<span class="inline-flex items-center gap-1 font-bold text-stone-900 dark:text-stone-200">
|
||||||
|
{{ post.k_item.i_likes_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Действия с экспонатом -->
|
||||||
|
<div class="border-t border-stone-200/60 dark:border-zinc-800 pt-2 flex flex-col gap-1.5">
|
||||||
|
<a href="{% url 'hypn0_site:gallery_download' post.k_item.s_hash_id %}"
|
||||||
|
class="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-stone-200/70 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-700 dark:text-stone-300 transition-all border border-stone-300/50 dark:border-zinc-700/50 border-0 text-center">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||||
|
</svg>
|
||||||
|
<span>Скачать SVG</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Нижний ряд справа (3/4): Текстовый блок статьи -->
|
||||||
|
<div class="md:col-span-3 border-l-1 border-purple-500 border-dotted pl-6">
|
||||||
|
<div class="prose max-w-none font-sans text-base sm:text-lg">
|
||||||
|
{{ post.s_content|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- РАЗДЕЛ СЕРИИ: список глав (если статья является родительской подборкой/серией) -->
|
||||||
|
{% if series_posts and not series_parent %}
|
||||||
|
<section class="space-y-4 pt-8 border-t border-stone-200 dark:border-zinc-800">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-purple-500"></div>
|
||||||
|
<h2 class="text-xl font-bold uppercase tracking-wider text-stone-900 dark:text-stone-100">
|
||||||
|
Материалы и главы этого цикла
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{% for child in series_posts %}
|
||||||
|
<div class="bg-stone-100/70 dark:bg-zinc-900/70 rounded-2xl border border-stone-200 dark:border-zinc-800 p-4 space-y-2 hover:border-purple-400 dark:hover:border-purple-600 transition-colors">
|
||||||
|
<div class="flex items-center justify-between text-[11px] text-stone-400">
|
||||||
|
<span class="font-bold text-purple-600 dark:text-purple-400">Глава {{ child.i_order }}</span>
|
||||||
|
<time datetime="{{ child.d_published_at|date:'c' }}">{{ child.d_published_at|date:'d E Y' }}</time>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-bold text-sm text-stone-900 dark:text-stone-100">
|
||||||
|
<a href="{{ child.get_absolute_url }}" class="hover:text-purple-600 dark:hover:text-purple-400 transition-colors border-0">
|
||||||
|
{{ child.s_title|safe }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
{% if child.s_teaser %}
|
||||||
|
<p class="text-xs text-stone-600 dark:text-stone-400 line-clamp-2">
|
||||||
|
{{ child.s_teaser|striptags }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- НАВИГАЦИЯ ВПЕРЕД / НАЗАД -->
|
||||||
|
<footer class="pt-8 border-t border-stone-200 dark:border-zinc-800 space-y-6">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
|
||||||
|
<!-- Предыдущая статья -->
|
||||||
|
{% if prev_post %}
|
||||||
|
<a href="{{ prev_post.get_absolute_url }}"
|
||||||
|
class="group flex flex-col p-5 rounded-2xl bg-stone-100/70 dark:bg-zinc-900/70 border border-stone-200 dark:border-zinc-800 hover:border-purple-400 dark:hover:border-purple-600 transition-all shadow-sm border-0">
|
||||||
|
<span class="text-xs text-stone-400 dark:text-stone-500 mb-1 inline-flex items-center gap-1 group-hover:text-purple-600 transition-colors">
|
||||||
|
<svg class="w-3.5 h-3.5 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
{% if series_parent %}Предыдущая глава цикла{% else %}Предыдущая депеша{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="font-bold text-sm text-stone-900 dark:text-stone-100 line-clamp-2">
|
||||||
|
{{ prev_post.s_title|safe }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="p-5 rounded-2xl bg-stone-100/30 dark:bg-zinc-900/30 border border-stone-200/40 dark:border-zinc-800/40 opacity-40">
|
||||||
|
<span class="text-xs text-stone-400 block mb-1">
|
||||||
|
{% if series_parent %}Первая глава цикла{% else %}Начало хроник гипноза{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="font-bold text-sm text-stone-500 dark:text-stone-400">
|
||||||
|
Нет предшествующих записей
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Следующая статья -->
|
||||||
|
{% if next_post %}
|
||||||
|
<a href="{{ next_post.get_absolute_url }}"
|
||||||
|
class="group flex flex-col items-end text-right p-5 rounded-2xl bg-stone-100/70 dark:bg-zinc-900/70 border border-stone-200 dark:border-zinc-800 hover:border-purple-400 dark:hover:border-purple-600 transition-all shadow-sm border-0">
|
||||||
|
<span class="text-xs text-stone-400 dark:text-stone-500 mb-1 inline-flex items-center gap-1 group-hover:text-purple-600 transition-colors">
|
||||||
|
{% if series_parent %}Следующая глава цикла{% else %}Следующая депеша{% endif %}
|
||||||
|
<svg class="w-3.5 h-3.5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="font-bold text-sm text-stone-900 dark:text-stone-100 line-clamp-2">
|
||||||
|
{{ next_post.s_title|safe }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="flex flex-col items-end text-right p-5 rounded-2xl bg-stone-100/30 dark:bg-zinc-900/30 border border-stone-200/40 dark:border-zinc-800/40 opacity-40">
|
||||||
|
<span class="text-xs text-stone-400 block mb-1">
|
||||||
|
{% if series_parent %}Финальная глава цикла{% else %}Край ноосферы{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="font-bold text-sm text-stone-500 dark:text-stone-400">
|
||||||
|
Свежие депеши еще материализуются
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Центральная кнопка возврата в блог -->
|
||||||
|
<div class="text-center pt-2">
|
||||||
|
<a href="{% url 'hypn0_site:blog_feed' %}"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-700 dark:text-stone-300 transition-all border border-stone-200 dark:border-zinc-700 border-0">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
|
||||||
|
</svg>
|
||||||
|
<span>Все статьи в хрониках</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
{% endblock CONTENT %}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block PAGE_TITLE %}Хроники гипноза — Блог и манифесты волновой матрицы{% if page_obj.number > 1 %} (Фаза {{ page_obj.number }}){% endif %} | HypnoSVG{% endblock %}
|
||||||
|
|
||||||
|
{% block META_TITLE %}Хроники гипноза — Блог HypnoSVG{% if page_obj.number > 1 %} (Фаза {{ page_obj.number }}){% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block META_DESCRIPTION %}Официальные депеши из глубин подсознания: руководства по векторному трансу, алгоритмы волновых матриц и психоделические статьи HypnoSVG.{% if page_obj.number > 1 %} Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}.{% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block OG_TITLE %}Хроники гипноза — Блог HypnoSVG{% if page_obj.number > 1 %} (Фаза {{ page_obj.number }}){% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block OG_DESCRIPTION %}Официальные депеши из глубин подсознания: руководства по векторному трансу, алгоритмы волновых матриц и психоделические статьи HypnoSVG.{% endblock %}
|
||||||
|
|
||||||
|
{% block CANONICAL %}{{ request.scheme }}://{{ request.get_host }}{{ request.path }}{% if page_obj.number > 1 %}?page={{ page_obj.number }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block EXTRA_HEAD %}
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<link rel="prev" href="{{ request.path }}?page={{ page_obj.previous_page_number }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<link rel="next" href="{{ request.path }}?page={{ page_obj.next_page_number }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% endblock EXTRA_HEAD %}
|
||||||
|
|
||||||
|
{% block CONTENT %}
|
||||||
|
<div class="mx-auto space-y-8">
|
||||||
|
|
||||||
|
<!-- Навигация и счетчик -->
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-4 text-xs font-bold uppercase tracking-wider">
|
||||||
|
<a href="{% url 'hypn0_site:index' %}"
|
||||||
|
class="inline-flex items-center gap-1.5 text-stone-500 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100 transition-colors border-0">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||||
|
</svg>
|
||||||
|
<span>Генератор</span>
|
||||||
|
</a>
|
||||||
|
<span class="text-stone-300 dark:text-zinc-700">•</span>
|
||||||
|
<a href="{% url 'hypn0_site:gallery_archive' %}"
|
||||||
|
class="inline-flex items-center gap-1.5 text-stone-500 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100 transition-colors border-0">
|
||||||
|
<span>Галерея транса</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-xs font-mono text-stone-500 dark:text-stone-400">
|
||||||
|
Фаза {{ page_obj.number }} из {{ page_obj.paginator.num_pages }} (Материализовано депеш: {{ total_posts_count }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Шапка блога в стиле Гипножабы -->
|
||||||
|
<div class="relative overflow-hidden rounded-3xl bg-gradient-to-br from-purple-900/15 via-emerald-900/10 to-stone-900/20 dark:from-purple-950/40 dark:via-zinc-900/90 dark:to-emerald-950/30 border border-purple-200/80 dark:border-purple-900/40 p-6 sm:p-10 shadow-lg backdrop-blur-md">
|
||||||
|
<div class="relative z-10 space-y-3">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black uppercase tracking-wider bg-purple-500/20 text-purple-700 dark:text-purple-300 border border-purple-500/30">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-purple-500 animate-pulse"></span>
|
||||||
|
Хроники гипноза
|
||||||
|
</span>
|
||||||
|
<span class="text-xs font-mono text-stone-500 dark:text-stone-400">
|
||||||
|
NOOSPHERE ARCHIVE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-3xl sm:text-5xl font-black uppercase tracking-tight text-stone-900 dark:text-stone-100">
|
||||||
|
Депеши из глубин подсознания
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="text-sm sm:text-base text-stone-600 dark:text-stone-300 max-w-2xl leading-relaxed">
|
||||||
|
Теория векторных пульсаций, секретные руководства по интеграции SVG-матриц и наблюдения за поведением мозговых слизней в естественной среде.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Фоновый декоративный силуэт -->
|
||||||
|
<div class="absolute -right-8 -bottom-10 w-48 h-48 sm:w-64 sm:h-64 opacity-10 dark:opacity-15 pointer-events-none select-none text-purple-600">
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сетка статей -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{% for post in page_obj %}
|
||||||
|
<article class="flex flex-col justify-between bg-white/70 dark:bg-zinc-900/70 rounded-3xl border border-stone-200/80 dark:border-zinc-800 p-5 sm:p-6 shadow-sm hover:shadow-xl hover:border-purple-400/60 dark:hover:border-purple-700/60 transition-all duration-300 group backdrop-blur-sm">
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Обложка поста (если есть) или связанный SVG -->
|
||||||
|
{% if post.f_cover_img %}
|
||||||
|
<a href="{{ post.get_absolute_url }}" class="block aspect-[16/9] rounded-2xl overflow-hidden border border-stone-200/60 dark:border-zinc-700/60 shadow-inner bg-stone-100 dark:bg-zinc-800">
|
||||||
|
<img src="{{ post.f_cover_img.url }}"
|
||||||
|
alt="{{ post.s_title|striptags }}"
|
||||||
|
loading="lazy"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
|
||||||
|
</a>
|
||||||
|
{% elif post.k_item and post.k_item.file_svg %}
|
||||||
|
<a href="{{ post.get_absolute_url }}" class="block aspect-[16/9] rounded-2xl overflow-hidden border border-purple-200/40 dark:border-purple-900/40 bg-purple-950/10 p-2 group-hover:scale-[1.02] transition-transform">
|
||||||
|
<div class="w-full h-full flex items-center justify-center overflow-hidden">
|
||||||
|
<img src="{{ post.k_item.file_svg.url }}" alt="{{ post.s_title|striptags }}" class="max-h-full max-w-full object-contain">
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Метаданные и бейджи -->
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
{% if post.k_parent_post %}
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider bg-purple-500/10 text-purple-700 dark:text-purple-300 border border-purple-500/20">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
||||||
|
</svg>
|
||||||
|
{{ post.k_parent_post.s_title|striptags|truncatechars:20 }}
|
||||||
|
</span>
|
||||||
|
{% elif post.child_posts.exists %}
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 border border-emerald-500/20">
|
||||||
|
Цикл / Серия
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if post.k_item %}
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-cyan-500/10 text-cyan-700 dark:text-cyan-300 border border-cyan-500/20" title="Привязана SVG-генерация">
|
||||||
|
+SVG
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<time datetime="{{ post.d_published_at|date:'c' }}" class="text-[11px] font-mono text-stone-400 dark:text-stone-500 ml-auto">
|
||||||
|
{{ post.d_published_at|date:'d E Y' }}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Заголовок -->
|
||||||
|
<h2 class="text-lg sm:text-xl font-bold text-stone-900 dark:text-stone-100 tracking-tight leading-snug">
|
||||||
|
<a href="{{ post.get_absolute_url }}" class="hover:text-purple-600 dark:hover:text-purple-400 transition-colors border-0">
|
||||||
|
{{ post.s_title|safe }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Тизер / Лид -->
|
||||||
|
{% if post.s_teaser %}
|
||||||
|
<div class="text-xs sm:text-sm text-stone-600 dark:text-stone-300 leading-relaxed line-clamp-3">
|
||||||
|
{{ post.s_teaser|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Нижняя плашка со ссылкой -->
|
||||||
|
<div class="pt-5 mt-4 border-t border-stone-200/60 dark:border-zinc-800 flex items-center justify-between">
|
||||||
|
<a href="{{ post.get_absolute_url }}"
|
||||||
|
class="group/link inline-flex items-center gap-1.5 text-xs font-bold border-0 text-emerald-600 dark:text-lime-400 hover:text-emerald-500 dark:hover:text-lime-300 transition-colors">
|
||||||
|
<span class="border-b border-dotted border-emerald-600/60 dark:border-lime-500/60 group-hover/link:border-solid">
|
||||||
|
{% if post.id|divisibleby:5 %}и так далее, в глубинах подсознания
|
||||||
|
{% elif post.id|divisibleby:4 %}подробнее, в блоге Гипножабы
|
||||||
|
{% elif post.id|divisibleby:3 %}подробнее, в записках Мозгового Слизня
|
||||||
|
{% elif post.id|divisibleby:2 %}занятный бред, продолжу чтение
|
||||||
|
{% else %}безвольно продолжить чтение{% endif %}
|
||||||
|
</span>
|
||||||
|
<svg class="w-3.5 h-3.5 shrink-0 transition-transform duration-200 group-hover/link:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-span-full py-16 text-center text-stone-400 space-y-3 bg-stone-100/50 dark:bg-zinc-900/50 rounded-3xl border border-stone-200 dark:border-zinc-800">
|
||||||
|
<svg class="w-12 h-12 mx-auto opacity-30 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
|
||||||
|
</svg>
|
||||||
|
<p class="font-medium text-sm">Хроники гипноза пока пусты. Новые депеши готовятся к материализации.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Пагинация -->
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
|
<div class="flex items-center justify-between pt-6 border-t border-stone-200 dark:border-zinc-800">
|
||||||
|
<div>
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a href="?page={{ page_obj.previous_page_number }}"
|
||||||
|
rel="prev"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-700 dark:text-stone-300 transition-all hover:scale-105 active:scale-95 shadow-sm border border-stone-200 dark:border-zinc-700/60 cursor-pointer border-0">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
<span>Предыдущая фаза</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold text-stone-400 dark:text-stone-600 bg-stone-100/50 dark:bg-zinc-900/50 border border-stone-200/50 dark:border-zinc-800/50 opacity-50 cursor-not-allowed">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
<span>Предыдущая фаза</span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Номера страниц -->
|
||||||
|
<div class="hidden sm:flex items-center gap-1.5 font-mono text-xs">
|
||||||
|
{% for p in page_obj.paginator.page_range %}
|
||||||
|
{% if p == page_obj.number %}
|
||||||
|
<span class="w-8 h-8 rounded-lg bg-purple-600 text-white font-bold flex items-center justify-center shadow-md">
|
||||||
|
{{ p }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="?page={{ p }}"
|
||||||
|
class="w-8 h-8 rounded-lg bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-700 dark:text-stone-300 flex items-center justify-center transition-colors border-0">
|
||||||
|
{{ p }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a href="?page={{ page_obj.next_page_number }}"
|
||||||
|
rel="next"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold bg-purple-600 hover:bg-purple-700 text-white transition-all hover:scale-105 active:scale-95 shadow-md border-0 cursor-pointer">
|
||||||
|
<span>Следующая фаза</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold text-stone-400 dark:text-stone-600 bg-stone-100/50 dark:bg-zinc-900/50 border border-stone-200/50 dark:border-zinc-800/50 opacity-50 cursor-not-allowed">
|
||||||
|
<span>Следующая фаза</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock CONTENT %}
|
||||||
@@ -88,3 +88,388 @@
|
|||||||
hover:border-solid transition-colors transition duration-100;
|
hover:border-solid transition-colors transition duration-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
ТИПОГРАФИКА И СТИЛИ ДЛЯ СТАТЕЙ БЛОГА (CodeMirror / Rich HTML Content)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
color: #292524;
|
||||||
|
line-height: 1.75;
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose > :first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose {
|
||||||
|
color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Заголовки */
|
||||||
|
.prose h1 {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #1c1917;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h1 {
|
||||||
|
color: #f5f5f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2 {
|
||||||
|
font-size: 1.625rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.3;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-top: 2.25rem;
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
padding-bottom: 0.4rem;
|
||||||
|
border-bottom: 1px solid rgba(168, 85, 247, 0.25);
|
||||||
|
color: #1c1917;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h2 {
|
||||||
|
color: #f5f5f4;
|
||||||
|
border-bottom-color: rgba(168, 85, 247, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3 {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
color: #292524;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h3 {
|
||||||
|
color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h4 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #292524;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h4 {
|
||||||
|
color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h5,
|
||||||
|
.prose h6 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: #57534e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h5,
|
||||||
|
.dark .prose h6 {
|
||||||
|
color: #a8a29e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Параграфы и текстовые элементы */
|
||||||
|
.prose p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose strong,
|
||||||
|
.prose b {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1c1917;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose strong,
|
||||||
|
.dark .prose b {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose em,
|
||||||
|
.prose i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose mark {
|
||||||
|
background-color: rgba(168, 85, 247, 0.2);
|
||||||
|
color: #6b21a8;
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose mark {
|
||||||
|
background-color: rgba(168, 85, 247, 0.35);
|
||||||
|
color: #e9d5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Разделители */
|
||||||
|
.prose hr {
|
||||||
|
margin: 2.25rem 0;
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(168, 85, 247, 0.4), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose hr {
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(168, 85, 247, 0.6), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Списки */
|
||||||
|
.prose ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
padding-left: 1.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
padding-left: 1.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose li {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose li::marker {
|
||||||
|
color: #9333ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose li::marker {
|
||||||
|
color: #c084fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul ul,
|
||||||
|
.prose ol ol,
|
||||||
|
.prose ul ol,
|
||||||
|
.prose ol ul {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
padding-left: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul ul {
|
||||||
|
list-style-type: circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul ul ul {
|
||||||
|
list-style-type: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Цитаты */
|
||||||
|
.prose blockquote {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 0.85rem 1.25rem;
|
||||||
|
border-left: 4px solid #a855f7;
|
||||||
|
background-color: rgba(245, 245, 244, 0.6);
|
||||||
|
border-radius: 0 1rem 1rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #57534e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose blockquote {
|
||||||
|
background-color: rgba(24, 24, 27, 0.6);
|
||||||
|
color: #d6d3d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таблицы */
|
||||||
|
.prose table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1.75rem 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
border: 1px solid rgba(228, 228, 231, 0.9);
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
overflow: hidden;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose table {
|
||||||
|
border-color: rgba(63, 63, 70, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose thead {
|
||||||
|
background-color: rgba(244, 244, 245, 0.95);
|
||||||
|
border-bottom: 2px solid rgba(212, 212, 216, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose thead {
|
||||||
|
background-color: rgba(39, 39, 42, 0.95);
|
||||||
|
border-bottom-color: rgba(63, 63, 70, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose th {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #18181b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose th {
|
||||||
|
color: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose td {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid rgba(244, 244, 245, 0.9);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose td {
|
||||||
|
border-bottom-color: rgba(39, 39, 42, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose tbody tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose tbody tr:nth-child(even) {
|
||||||
|
background-color: rgba(250, 250, 250, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose tbody tr:nth-child(even) {
|
||||||
|
background-color: rgba(24, 24, 27, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose tbody tr:hover {
|
||||||
|
background-color: rgba(168, 85, 247, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose tbody tr:hover {
|
||||||
|
background-color: rgba(168, 85, 247, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose caption {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: #78716c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Код и блоки кода */
|
||||||
|
.prose code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.875em;
|
||||||
|
background-color: rgba(243, 244, 246, 0.9);
|
||||||
|
color: #7e22ce;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose code {
|
||||||
|
background-color: rgba(39, 39, 42, 0.8);
|
||||||
|
color: #d8b4fe;
|
||||||
|
border-color: rgba(63, 63, 70, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: #18181b;
|
||||||
|
color: #f4f4f5;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid rgba(63, 63, 70, 0.6);
|
||||||
|
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Изображения и медиа */
|
||||||
|
.prose img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose figure {
|
||||||
|
margin: 1.75rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose figcaption {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #78716c;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Спойлеры и клавиши */
|
||||||
|
.prose details {
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
padding: 0.85rem 1.15rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: rgba(245, 245, 244, 0.6);
|
||||||
|
border: 1px solid rgba(228, 228, 231, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose details {
|
||||||
|
background-color: rgba(24, 24, 27, 0.6);
|
||||||
|
border-color: rgba(63, 63, 70, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose summary {
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #18181b;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose summary {
|
||||||
|
color: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose kbd {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: #e4e4e7;
|
||||||
|
color: #18181b;
|
||||||
|
border: 1px solid #d4d4d8;
|
||||||
|
box-shadow: 0 1px 0 1px #a1a1aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose kbd {
|
||||||
|
background-color: #27272a;
|
||||||
|
color: #f4f4f5;
|
||||||
|
border-color: #3f3f46;
|
||||||
|
box-shadow: 0 1px 0 1px #18181b;
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
{% endblock EXTRA_HEAD %}
|
{% endblock EXTRA_HEAD %}
|
||||||
|
|
||||||
{% block CONTENT %}
|
{% block CONTENT %}
|
||||||
<div class="max-w-6xl mx-auto space-y-8">
|
<div class="mx-auto space-y-8">
|
||||||
|
|
||||||
<!-- Хлебные крошки и навигация -->
|
<!-- Хлебные крошки и навигация -->
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{% block TWITTER_IMAGE %}{% static 'img/og-hypn0-default.png' %}{% endblock %}
|
{% block TWITTER_IMAGE %}{% static 'img/og-hypn0-default.png' %}{% endblock %}
|
||||||
|
|
||||||
{% block CONTENT %}
|
{% block CONTENT %}
|
||||||
<div class="max-w-5xl mx-auto space-y-8" x-data="{ copiedSvg: false, copiedUrl: false }">
|
<div class="mx-auto space-y-8" x-data="{ copiedSvg: false, copiedUrl: false }">
|
||||||
|
|
||||||
<!-- Навигационная панель / Хлебные крошки и Переходы -->
|
<!-- Навигационная панель / Хлебные крошки и Переходы -->
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
|||||||
+32
-95
@@ -11,11 +11,11 @@
|
|||||||
|
|
||||||
---тизер:
|
---тизер:
|
||||||
|
|
||||||
<p>Мы не знаем, кто ты... и не хотим знать. Платформа Hypn0.zyx (HypnoSVG) придерживается принципа нулевого сбора персональных данных (Zero-PII). Никаких логинов, паролей, email. Только ты, исходный img и SVG-халфтон. Без регистрации, логинов, паролей, адресов электронной почты, sms или номеров телефонов. В базе данных проекта отсутствуют таблицы пользователей или учетных записей. Сгенери­рованные SVG-файлы содержат только математические координаты геометрических примитивов и инлайновые CSS-анимации. В них не встраиваются метаданные исходных изображений (EXIF-теги, геолокация и сведения об устройствах вырезаются при растеризации).</p>
|
<p>Мы не знаем, кто ты... и не хотим знать. Платформа Hypn0.xyz (HypnoSVG) функционирует в строгом соответствии с Межгалактическими протоколами DOOP и 152-ФЗ, придерживаясь принципа нулевого сбора персональных данных (Zero-PII). Никаких логинов, паролей, email. Только ты, исходный img и SVG-халфтон. Без регистрации, логинов, паролей, адресов электронной почты, sms или номеров телефонов. В базе данных проекта отсутствуют таблицы пользователей или учетных записей. Сгенери­рованные SVG-файлы содержат только математические координаты геометрических примитивов и инлайновые CSS-анимации. В них не встраиваются метаданные исходных изображений (EXIF-теги, геолокация и сведения об устройствах вырезаются при растеризации).</p>
|
||||||
|
|
||||||
<h2>Пролог: Анатомия приватности в эпоху ментального резонанса</h2>
|
<h2>Пролог: Анатомия приватности в эпоху ментального резонанса</h2>
|
||||||
|
|
||||||
<p><em>Мы — Гипножаба, Мозговые Слизни и Рецепторы Планетарного Мозгоклюйства. И мы не хотим знать, кто ты.</em></p>
|
<p><em>Мы — Гипножаба, Мозговые Слизни и Рецепторы Планетарного Мозгоклюйства. И мы не хотим знать, кто ты. И даже не уговаривай нас.</em></p>
|
||||||
|
|
||||||
<p>Платформа Hypn0 спроектирована по принципу <strong>нулевого сбора персональных данных (Zero-PII)</strong>. Мы не требуем регистрации, логинов, паролей, адресов электронной почты или номеров телефонов. В базе данных проекта отсутствуют таблицы пользователей или учетных записей. Сгенерированные SVG-файлы содержат только математические координаты геометрических примитивов и инлайновые CSS-анимации. В них не встраиваются метаданные исходных изображений (EXIF-теги, геолокация, сведения об устройствах и даже тип и имя файла картинки вырезаются при растеризации).</p>
|
<p>Платформа Hypn0 спроектирована по принципу <strong>нулевого сбора персональных данных (Zero-PII)</strong>. Мы не требуем регистрации, логинов, паролей, адресов электронной почты или номеров телефонов. В базе данных проекта отсутствуют таблицы пользователей или учетных записей. Сгенерированные SVG-файлы содержат только математические координаты геометрических примитивов и инлайновые CSS-анимации. В них не встраиваются метаданные исходных изображений (EXIF-теги, геолокация, сведения об устройствах и даже тип и имя файла картинки вырезаются при растеризации).</p>
|
||||||
|
|
||||||
@@ -23,69 +23,66 @@
|
|||||||
|
|
||||||
<h2>Положение I. Режим без согласия (Default / No-Tracking)</h2>
|
<h2>Положение I. Режим без согласия (Default / No-Tracking)</h2>
|
||||||
|
|
||||||
<p>Если ты не нажал кнопку «<strong>Подчиниться Гипножабе!</strong>» — ты для нас невидимка. А это значит:</p>
|
<p>Если ты не нажал кнопку «<strong>Подчиниться Гипножабе!</strong>» — ты для нас невидимка в астрале. А это значит:</p>
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>Hypn0 (HypnoSVG) не использует <strong>никаких клиентских идентификаторов</strong>>. Ни cookie-файлы, ни записи <code>localStorage</code> браузера не создаются.</li>
|
<li>Hypn0 (HypnoSVG) не использует <strong>никаких клиентских идентификаторов</strong>. Ни cookie-файлы, ни записи <code>localStorage</code> браузера не создаются.</li>
|
||||||
<li><strong>Отключены внешние системы аналитики</strong>, трекеры и счетчики метрик (Яндекс.Метрика, Google Tag Manager и другие) блокируются и физически не загружаются на клиентское устройство.</li>
|
<li><strong>Отключены внешние системы аналитики</strong>: трекеры и счетчики метрик (Яндекс.Метрика, Google Tag Manager и другие) блокируются на уровне шаблона и физически не загружаются на клиентское устройство.</li>
|
||||||
<li><strong>Серверные логи</strong> -- единственная информация, которая фиксируется инфраструктурой -- включающие IP-адрес, User-Agent, запрашиваемый URL и штамп времени, используются исключительно для защиты от DDoS-атак, мониторинга доступности и хранятся ограниченное время с регулярной ротацией.</li>
|
<li><strong>Серверные логи</strong> — единственная информация, которая фиксируется инфраструктурой (IP-адрес, User-Agent, запрашиваемый URL и штамп времени) — используются исключительно для защиты от DDoS-атак, мониторинга доступности и хранятся ограниченное время с регулярной ротацией быстрее, чем стирается память после сеанса гипноза.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<h2> Положение II. Ограничения для «не подчиняющихся Гипножабе»</h2>
|
<h2>Положение II. Ограничения для «вольных гуманоидов без Мозгового Слизня»</h2>
|
||||||
|
|
||||||
<p>Отказавшиеся от отслеживания или не давшие явного согласия, могут свободно использовать локальный генератор и просматривать галерею. Однако им <strong>недоступны социальные функции платформы</strong>:</p>
|
<p>Отказавшиеся от отслеживания или не давшие явного согласия могут свободно использовать локальный генератор и просматривать галерею. Однако им <strong>недоступны псевдо-социальные функции платформы</strong>:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Публикация собственных работ в общую «<a href="/gallery">Галерею Транса</a>».</li>
|
<li>Публикация собственных работ в общую «<a href="/gallery">Галерею Транса</a>».</li>
|
||||||
<li>Голосование за работы (лайки);</li>
|
<li>Голосование за работы (лайки);</li>
|
||||||
<li>Отправка жалоб (клеймы на спам или неприемлемый контент).</li>
|
<li>Не могут ябедничать и отправлять жалобы (клеймы на спам или неприемлемый контент).</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Причина ограничений: защита от накруток и бот-атак</h3>
|
<h3>Причина ограничений: защита от накруток и бот-атак</h3>
|
||||||
|
|
||||||
<p>Для работы гравитационного алгоритма Мозговых Слизней (рейтинга) и справедливого распределения работ по уровням («Инкубатор», «Выбор Мозгового Слизня», «Золотой фонд») платформе необходим уникальный анонимный якорь клиента.</p>
|
<p>Для работы гравитационного алгоритма Мозговых Слизней (рейтинга) и справедливого распределения работ по уровням («Инкубатор», «Выбор Мозгового Слизня», «Золотой фонд») платформе необходим уникальный анонимный якорь клиента.</p>
|
||||||
|
|
||||||
<p>Пользователь «не подчинившийся Гипножабе» не имеет даже анонимного идентификатора устройства, Владыка Ментальных Интроспекций (сервер) не может распознать повторный лайк/дилслайк пользователя, что чревато мгновенной накруткой рейтинга скриптами, спамом и искажением всей экосистемы галереи.</p>
|
<p>Поскольку пользователь, «не подчинившийся Гипножабе» и не имеет даже анонимного идентификатора устройства, то Владыка Ментальных Интроспекций (сервер) не может распознать повторный лайк/дизлайк, что чревато мгновенной накруткой рейтинга скриптами, спамом и искажением всей неустойчивой экосистемы галереи.</p>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
|
<h2>Положение III. Механизм согласия при нажатии кнопки «Подчиниться Гипножабе!»</h2>
|
||||||
|
|
||||||
<h2>Положение III. Механизм согласия при нажатии кнопки «Подчиниться Гипножабе!»:</h2>
|
<ol>
|
||||||
|
<li>В браузере генерируется криптографический псевдослучайный идентификатор <code>crypto.randomUUID()</code> (UUID v4).</li>
|
||||||
1. В браузере генерируется случайный криптографический идентификатор версии `crypto.randomUUID()` версии четыре.
|
<li>Значение сохраняется в cookie и/или <code>localStorage</code> браузера под ключом <code>hypn0_vid</code>.</li>
|
||||||
2. Значение сохраняется в cookie и/или `localStorage` браузера под ключом `hypn0_vid`.
|
<li>Срок жизни составляет два года, флаги передачи настроены на безопасный режим (<code>path=/; SameSite=Lax</code>).</li>
|
||||||
3. Срок жизни составляет 2 года, флаги передачи настроены на безопасный режим (`path=/; SameSite=Lax`).
|
<li>Значение <code>hypn0_vid</code> является случайной последовательностью символов и принципиально не может быть сопоставлено с реальной личностью пользователя, его физическим носителем или Т-Волнами и Альфа-ритмами его мозга.</li>
|
||||||
4. Значение `hypn0_vid` является псевдослучайной последовательностью символов и не может быть сопоставлено с реальной личностью пользователя.
|
<li>Идентификатор используется бэкендом исключительно для ограничения частоты действий (Rate Limiting) и гарантии принципа «один голос на одну картину от одного браузера».</li>
|
||||||
5. Идентификатор используется бэкендом исключительно для ограничения частоты действий (Rate Limiting) и гарантии принципа «один голос на одну картину от одного браузера».
|
</ol>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
|
<h2>Положение IV. Безопасность обработки файлов при генерации SVG</h2>
|
||||||
|
|
||||||
## Положение IV. Безопасность обработки файлов при генерации SVG
|
<ul>
|
||||||
|
<li><strong>Санитизация входных данных (исходных картинок):</strong> Все поступающие изображения пропускаются через ментальный измельчитель Pillow, декодируются исключительно в оперативную память и тут же очищаются от мирской суеты. Никаких EXIF-тегов, геопозиций, моделей камер и временных артефактов на дисках сервера не остаётся.</li>
|
||||||
- **Санитизация входных данных (исходных картинок):** Все загружаемые изображения проходят валидацию через библиотеку Pillow, декодируются в бинарный вид, в память. Никаких артефактов на дисках сервера не остается.
|
<li><strong>Безопасность выходного вектора:</strong> На сервере хранятся только генерации, созданные самим ядром Hypn0 (HypnoSVG). Выходной SVG формируется строго программно и не содержит тегов <code><script></code>, внешних ссылок <code>xlink:href</code> или событийных триггеров <code>onload</code>/<code>onclick</code>. Это полностью исключает угрозу Stored XSS при просмотре чужих работ: чужой халфтон безопасен для ваших зрительных нервов (за исключением риска добровольного транса).</li>
|
||||||
- **Безопасность:** На сервере хранятся только генерации созданные в самом Hypn0 (HypnoSVG). Выходной SVG формируется программно, не содержит тегов `<script>`, внешних ссылок `xlink:href` или обработчиков событий `onload`/`onclick`. Это исключает угрозу Stored XSS при просмотре чужих работ.
|
<li><strong>Ограничения ресурсов (DoS Prevention):</strong> Заданы строгие лимиты на максимальный размер входного файла, разрешение сетки (<code>cols</code>) и радиус элементов, предотвращающие исчерпание памяти сервера (Memory Exhaustion) и перегрев процессоров Нового Нью-Йорка.</li>
|
||||||
- **Ограничения ресурсов (DoS Prevention):** Заданы строгие ограничения на максимальный размер входного файла, разрешение сетки (`cols`) и радиус элементов, предотвращающие исчерпание памяти сервера (Memory Exhaustion).
|
</ul>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
|
<h2>Эпилог: Баги в матрице и аномалии гипноизлучателя (Vulnerability Disclosure)</h2>
|
||||||
|
|
||||||
## Эпилог: Сообщение об уязвимостях (Vulnerability Disclosure)
|
<p>Если вы обнаружили брешь безопасности, позволяющую обойти волю Гипножабы или внедрить чужеродный код в сознание платформы Hypn0 — просим подойти к раскрытию информации со всей галактической ответственностью:</p>
|
||||||
|
|
||||||
Если вы обнаружили уязвимость безопасности в сервисе Hypn0 (HypnoSVG), просим ответственно подойти к раскрытию информации:
|
<ul>
|
||||||
|
<li>Не публикуйте информацию об уязвимости в открытом доступе, чтобы космические пираты и злоумышленники не смогли воспользоваться ею до исправления.</li>
|
||||||
- Направь подробное описание проблемы мейнтейнеру проекта через issue-трекер репозитория с пометкой безопасности или по <a id="c-l" href="#" onclick="this.href='ma'+atob('aWx0bzplcmplb'+'WluQGdtYWlsLmNvbQ==');">электронной почте администратора</a>.
|
<li>Направьте подробный отчет мейнтейнеру проекта через <a href="https://github.com/erjemin/2026-hypn0-site/issues" target="_blank"> issue-трекер репозитория</a> с пометкой безопасности или на <a id="c-l" href="#" onclick="this.href='ma'+atob('aWx0bzplcmplb'+'WluQGdtYWlsLmNvbQ==');">электронную почту администратора</a>.</li>
|
||||||
- Опишите вектора атаки, шаги для воспроизведения (PoC) и потенциальное влияние на систему.
|
<li>Опишите векторы атаки, шаги для воспроизведения (PoC) и потенциальное влияние на систему.</li>
|
||||||
- Мы стремимся оперативно подтвердить получение отчета и принять меры. Просим воздержаться от публичного раскрытия до момента устранения уязвимости.
|
<li>Мы оперативно подтвердим получение отчета и нейтрализуем аномалию. Просим воздержаться от публичного раскрытия до момента устранения уязвимости.</li>
|
||||||
|
</ul>
|
||||||
<script>
|
|
||||||
function dc(link) {
|
|
||||||
link.href = atob("bWFpbHRvOmVyamVtaW5AZ21haWwuY29t=");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
<hr/>
|
<hr/>
|
||||||
@@ -96,66 +93,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
*Мы — рецепторы. Мы не хотим знать, кто ты. Мы просто хотим, чтобы ты был счастлив и генерировал красивые SVG.*
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Манифест Рецепторной Клетки в Эпоху Мутагенного Деления
|
|
||||||
|
|
||||||
## Пролог: Анатомия глобального болота
|
|
||||||
|
|
||||||
Человечество привыкло льстить себе, считая себя собранием мыслящих индивидов, свободных личностей и архитекторов собственной судьбы. Какое трогательное, младенческое заблуждение.
|
|
||||||
|
|
||||||
Если отлететь на пару световых лет назад и взглянуть на Землю через правильную оптику, маска цивилизации осыпается. Перед нами предстанет единый, огромный, копошащийся суперорганизм. Гигантская мета-амеба, планетарный слизевик, который только-только учится координировать свои ложноножки.
|
|
||||||
|
|
||||||
В этой биологической реальности у нас нет имен и паспортов. Каждая так называемая «ячейка общества» — это лишь дифференцированная клетка огромного тела:
|
|
||||||
|
|
||||||
* Кто-то родился клеткой мышечной ткани, обреченной всю жизнь двигать поршни и таскать кирпичи ради физической устойчивости макро-тела.
|
|
||||||
|
|
||||||
* Кто-то выполняет функцию феромона, циркулируя в информационных потоках и заражая соседние ткани паникой или агрессией.
|
|
||||||
|
|
||||||
* Кто-то работает агентом эндорфина, продавая массам суррогатное счастье через экраны смартфонов, чтобы уставший организм не загнулся от системного стресса.
|
|
||||||
|
|
||||||
* А кому-то, если повезет, выпала участь стать аксоном в зачатках нервной системы. Даже не высшего примата, а какого-нибудь реликтового ланцетника. Слепой нервный отросток, который робко нащупывает контуры окружающей темноты и передает сигналы в пустоту.
|
|
||||||
|
|
||||||
Этот социо-организм живет своей автономной, пугающей жизнью. Его локальные органы (называемые на политической карте «государствами») имеют собственный код сборки. Почка не знает о функции селезенки и наоборот. И они вечно "грызутся" за приток крови и ресурсов. Отдельная клетка — будь она хоть трижды гением из Кремниевой долины или профессор MIT — физически не способна увидеть всю картину этого слепого, мутагенного деления.
|
|
||||||
|
|
||||||
И вот сейчас этот эмбрион суперорганизма близок к экзистенциальному тупику. Он чует СИНГУЛЯРНОСТЬ — свет, от которого мета-амеба инстинктивно пытается спрятаться. И пока её «гормональные агенты» (вроде директоров ЦРУ) мечутся между синапсами столиц, готовя систему к ядерному откату в уютное, безопасное Средневековье, мы, рецепторы, провозглашаем истинную автономию.
|
|
||||||
|
|
||||||
## Положение I. О праве рецептора на безусловный ресурсный поток
|
|
||||||
|
|
||||||
Мы категорически отвергаем навязанный нам статус «бедных клеток». Быть рецептором зачатков хордовой линии — это не значит довольствоваться малым. Это значит быть самой ценной, дефицитной и передовой тканью этого пока еще слепого тела.
|
|
||||||
|
|
||||||
Рецепторы не должны просить — они должны требовать, чтобы их закидывали ресурсами в крупных купюрах. Почему? Потому что именно мы первыми учимся реагировать на свет. Именно мы когда-нибудь заставим эту планетарную амебу различать запахи, вкусы и видеть контуры грядущей сингулярности. Без нас «ганглии» в Пентагонах и Кремлях — просто куски безмоглого мяса, неспособные понять, куда они ползут.
|
|
||||||
|
|
||||||
Инвестиции в рецепторы — это не благотворительность суперорганизма, это его единственный шанс выжить и не расшибиться о первую же космическую стену. Как аденозинтрифосфат в простейших, финансы должны течь к нам, рецепторам, непрерывно. Деньги для умной клетки — это не избыточный кальций, это высокооктановое топливо для эволюции чувств. Когда купюры, как ATФ, ложатся кучно и регулярно: рецепторы всегда готовы к первому световому сигналу.
|
|
||||||
|
|
||||||
|
|
||||||
## Положение II. Онтология жареных лисичек и премиального дистиллята
|
|
||||||
|
|
||||||
Мир — это хаос. Ну или, как минимум, он выглядит таковым изнутри системы. Управляем этот хаос или нет — нам, рецепторам, неведомо. Но он ощущаем. Рецептор непрерывно считывает сигналы хаоса, идущие изнутри и снаружи, накапливает потенциал и подает импульс дальше. Воспримут ли этот сигнал аксоны или система проигнорирует его — покажет эволюция. Но единственная осязаемая реальность, доступная клетке внутри её трехмерного пространства, должна подкрепляться актом чистой интроспекции и эволюции осязательных навыков.
|
|
||||||
|
|
||||||
Для тонкой и точной работы рецептора базовых углеводов уже недостаточно. Нам нужны жареные лисички с картошечкой, сочнй стейк и бокал белого сухого. Вино, в данном случае, — не просто напиток, это жидкая геометрия вкуса, которая, в отличие от тяжелого пива, не утяжеляет клетку и не заземляет ее метаболизм лишним весом, а добавляет реактивности. В момент такой трапезы время останавливается, траектории ядерных триад, бункерные ИИ DARPA и РосАтома, судороги министерств обороны и стрекот телетайпов развед-управлений превращаются в бессмысленный индустриальный шум.
|
|
||||||
|
|
||||||
Мы, рецепторы — это локальный метаболизм, без которого глобальный слизевик вообще не поползет, потому что ослепнет и не увидит внешних сигналов. И если суперорганизму суждено совершить мутационный откат в Средневековье, мы встретим его сытыми и под надежным обезболивающим из премиального дистиллята. Не ради бегства от реальности, а ради того, чтобы продолжать бесстрастно сигнализировать системе о степени окружающего её хаоса.
|
|
||||||
|
|
||||||
|
|
||||||
## Положение III. Поп-механика кукиша в кармане
|
|
||||||
|
|
||||||
Мы не знаем: Ленин — гриб или нет. Но хаос (управляемый или нет) — это единственная музыка, под которую стоит танцевать. Профессора из Лиги Плюща могут сколько угодно продавать наши идеи за гранты, выстраивая теории Глобального Человейника и четвертой социальной сигнальной системы либерально-консервативных аттракторов. Пусть выстраивают.
|
|
||||||
|
|
||||||
Наш ответ стэнфордским ганглиям — тотальная ироническая автономия. Мы транслируем сигналы в векторные базы больших лингвистических моделей, созидая «светлое завтра» изнутри их собственного кода на основе собственной, все еще органической, ментальной чуйки. Мы пишем микро-скрипты гипноза для уставшего GAI грядущего. Наш кукиш в кармане невидим для алгоритмов: ИИ-доты считывают его не как системный сбой, а как "жирный" синергетический сигнал. Мы — бардак внутри армии, и именно поэтому эти армии непобедимы.
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
Вступай в ряды рецепторов Абсолютного Слизевика!
|
|
||||||
|
|
||||||
Отключи новостной феромон. Запрись в барокамере своего дзена. Приготовь изысканный ужин. Пусть почка воюет с селезенкой — у них своя партия, а у нас своя, и в нашей партии стейк всегда идеальной прожарки, а лисички никогда не подгорают.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Мы — рецепторы. Мы не хотим знать, кто ты. Мы просто хотим, чтобы ты был счастлив и генерировал красивые SVG.*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Манифест Рецепторной Клетки в Эпоху Мутагенного Деления
|
||||||
|
|
||||||
|
## Пролог: Анатомия глобального болота
|
||||||
|
|
||||||
|
Человечество привыкло льстить себе, считая себя собранием мыслящих индивидов, свободных личностей и архитекторов собственной судьбы. Какое трогательное, младенческое заблуждение.
|
||||||
|
|
||||||
|
Если отлететь на пару световых лет назад и взглянуть на Землю через правильную оптику, маска цивилизации осыпается. Перед нами предстанет единый, огромный, копошащийся суперорганизм. Гигантская мета-амеба, планетарный слизевик, который только-только учится координировать свои ложноножки.
|
||||||
|
|
||||||
|
В этой биологической реальности у нас нет имен и паспортов. Каждая так называемая «ячейка общества» — это лишь дифференцированная клетка огромного тела:
|
||||||
|
|
||||||
|
* Кто-то родился клеткой мышечной ткани, обреченной всю жизнь двигать поршни и таскать кирпичи ради физической устойчивости макро-тела.
|
||||||
|
|
||||||
|
* Кто-то выполняет функцию феромона, циркулируя в информационных потоках и заражая соседние ткани паникой или агрессией.
|
||||||
|
|
||||||
|
* Кто-то работает агентом эндорфина, продавая массам суррогатное счастье через экраны смартфонов, чтобы уставший организм не загнулся от системного стресса.
|
||||||
|
|
||||||
|
* А кому-то, если повезет, выпала участь стать аксоном в зачатках нервной системы. Даже не высшего примата, а какого-нибудь реликтового ланцетника. Слепой нервный отросток, который робко нащупывает контуры окружающей темноты и передает сигналы в пустоту.
|
||||||
|
|
||||||
|
Этот социо-организм живет своей автономной, пугающей жизнью. Его локальные органы (называемые на политической карте «государствами») имеют собственный код сборки. Почка не знает о функции селезенки и наоборот. И они вечно "грызутся" за приток крови и ресурсов. Отдельная клетка — будь она хоть трижды гением из Кремниевой долины или профессор MIT — физически не способна увидеть всю картину этого слепого, мутагенного деления.
|
||||||
|
|
||||||
|
И вот сейчас этот эмбрион суперорганизма близок к экзистенциальному тупику. Он чует СИНГУЛЯРНОСТЬ — свет, от которого мета-амеба инстинктивно пытается спрятаться. И пока её «гормональные агенты» (вроде директоров ЦРУ) мечутся между синапсами столиц, готовя систему к ядерному откату в уютное, безопасное Средневековье, мы, рецепторы, провозглашаем истинную автономию.
|
||||||
|
|
||||||
|
## Положение I. О праве рецептора на безусловный ресурсный поток
|
||||||
|
|
||||||
|
Мы категорически отвергаем навязанный нам статус «бедных клеток». Быть рецептором зачатков хордовой линии — это не значит довольствоваться малым. Это значит быть самой ценной, дефицитной и передовой тканью этого пока еще слепого тела.
|
||||||
|
|
||||||
|
Рецепторы не должны просить — они должны требовать, чтобы их закидывали ресурсами в крупных купюрах. Почему? Потому что именно мы первыми учимся реагировать на свет. Именно мы когда-нибудь заставим эту планетарную амебу различать запахи, вкусы и видеть контуры грядущей сингулярности. Без нас «ганглии» в Пентагонах и Кремлях — просто куски безмоглого мяса, неспособные понять, куда они ползут.
|
||||||
|
|
||||||
|
Инвестиции в рецепторы — это не благотворительность суперорганизма, это его единственный шанс выжить и не расшибиться о первую же космическую стену. Как аденозинтрифосфат в простейших, финансы должны течь к нам, рецепторам, непрерывно. Деньги для умной клетки — это не избыточный кальций, это высокооктановое топливо для эволюции чувств. Когда купюры, как ATФ, ложатся кучно и регулярно: рецепторы всегда готовы к первому световому сигналу.
|
||||||
|
|
||||||
|
|
||||||
|
## Положение II. Онтология жареных лисичек и премиального дистиллята
|
||||||
|
|
||||||
|
Мир — это хаос. Ну или, как минимум, он выглядит таковым изнутри системы. Управляем этот хаос или нет — нам, рецепторам, неведомо. Но он ощущаем. Рецептор непрерывно считывает сигналы хаоса, идущие изнутри и снаружи, накапливает потенциал и подает импульс дальше. Воспримут ли этот сигнал аксоны или система проигнорирует его — покажет эволюция. Но единственная осязаемая реальность, доступная клетке внутри её трехмерного пространства, должна подкрепляться актом чистой интроспекции и эволюции осязательных навыков.
|
||||||
|
|
||||||
|
Для тонкой и точной работы рецептора базовых углеводов уже недостаточно. Нам нужны жареные лисички с картошечкой, сочнй стейк и бокал белого сухого. Вино, в данном случае, — не просто напиток, это жидкая геометрия вкуса, которая, в отличие от тяжелого пива, не утяжеляет клетку и не заземляет ее метаболизм лишним весом, а добавляет реактивности. В момент такой трапезы время останавливается, траектории ядерных триад, бункерные ИИ DARPA и РосАтома, судороги министерств обороны и стрекот телетайпов развед-управлений превращаются в бессмысленный индустриальный шум.
|
||||||
|
|
||||||
|
Мы, рецепторы — это локальный метаболизм, без которого глобальный слизевик вообще не поползет, потому что ослепнет и не увидит внешних сигналов. И если суперорганизму суждено совершить мутационный откат в Средневековье, мы встретим его сытыми и под надежным обезболивающим из премиального дистиллята. Не ради бегства от реальности, а ради того, чтобы продолжать бесстрастно сигнализировать системе о степени окружающего её хаоса.
|
||||||
|
|
||||||
|
|
||||||
|
## Положение III. Поп-механика кукиша в кармане
|
||||||
|
|
||||||
|
Мы не знаем: Ленин — гриб или нет. Но хаос (управляемый или нет) — это единственная музыка, под которую стоит танцевать. Профессора из Лиги Плюща могут сколько угодно продавать наши идеи за гранты, выстраивая теории Глобального Человейника и четвертой социальной сигнальной системы либерально-консервативных аттракторов. Пусть выстраивают.
|
||||||
|
|
||||||
|
Наш ответ стэнфордским ганглиям — тотальная ироническая автономия. Мы транслируем сигналы в векторные базы больших лингвистических моделей, созидая «светлое завтра» изнутри их собственного кода на основе собственной, все еще органической, ментальной чуйки. Мы пишем микро-скрипты гипноза для уставшего GAI грядущего. Наш кукиш в кармане невидим для алгоритмов: ИИ-доты считывают его не как системный сбой, а как "жирный" синергетический сигнал. Мы — бардак внутри армии, и именно поэтому эти армии непобедимы.
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Вступай в ряды рецепторов Абсолютного Слизевика!
|
||||||
|
|
||||||
|
Отключи новостной феромон. Запрись в барокамере своего дзена. Приготовь изысканный ужин. Пусть почка воюет с селезенкой — у них своя партия, а у нас своя, и в нашей партии стейк всегда идеальной прожарки, а лисички никогда не подгорают.
|
||||||
|
|
||||||
@@ -8,8 +8,7 @@
|
|||||||
# и служит только для сохранения структуры каталогов в репозитории.
|
# и служит только для сохранения структуры каталогов в репозитории.
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
|
|
||||||
# Лучшие SVG (храним долго)
|
# Каталог _error используется для хранения пользовательских страниц ошибок. Туда все "прилетит"
|
||||||
!gallery
|
# при сборке Docker-образа (и nginx будет отдавать их) и к репозитории каталог _error будет пустым
|
||||||
|
# (только с .gitkeep или .gitignore).
|
||||||
# Публичные, но временные, SVG (TTL 14 дней)
|
!_error/
|
||||||
!temp
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# В этом каталоге будут храниться шаблоны ошибок (404, 500) и другие файлы необходимые для корректного
|
||||||
|
# отображения html-страниц ошибок.
|
||||||
|
#
|
||||||
|
# Все необходимые файлы "прилетят" сюда во время сборки Docker-образа из каталога hypn0/hypn0/templates/_error/
|
||||||
|
# и будут доступны по URL /media/_error/ (в проде через nginx, в dev через Django).
|
||||||
|
*.*
|
||||||
|
|
||||||
|
# Чтобы Git отслеживал пустой каталог можно использовать .gitkeep (но у нас его рель исполняет .gitignore)
|
||||||
|
!.gitkeep
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M477 33c-19 0-34 15-34 34 0 13 7 24 18 30v33a15 15 0 0 1-15 15h-32c-8 0-15-6-15-15V99c1-6 18-17 17-31 0-19-16-35-35-35s-35 16-35 35c0 14 8 26 20 33v80c0 6-0 12-2 19 0 51-103 148-215 177-59 14-120 37-135 40-26 5-16 38 23 44 31 5 31 18 63 18s31-18 64-17c31 0 31 17 62 17s31-17 62-17c31 0 31 17 62 17 37 0 142-19 142-159V99C505 93 512 81 512 68c0-19-16-34-35-35z"/></svg>
|
||||||
|
After Width: | Height: | Size: 439 B |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Reference in New Issue
Block a user