30 Commits
Author SHA1 Message Date
erjemin 7c44f2ef35 mod: оптимизация загрузки (svg-картинки подгружаются динамически при прокрутке, с джиттером и т.п.)
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 33s
2026-09-25 21:35:57 +03:00
erjemin 5e9d4e0902 fix: Буферизация 2026-09-20 22:30:13 +03:00
erjemin 5f53be810f fix: CI/CD сборка tailwind для прода (устранена ошибка версий)
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 1m23s
2026-09-19 18:57:13 +03:00
erjemin 29a063c806 mod: design adjust (10) - улучшена адаптивность шапки 2026-09-19 18:31:50 +03:00
erjemin d16a5459ad mod: design adjust (9) - улучшена адаптивность главной страницы 2026-09-19 18:15:55 +03:00
erjemin ddb0f9ebbb mod: design adjust (8) - улучшена адаптивность предупреждения об отслеживании для мобильных. 2026-09-19 16:35:44 +03:00
erjemin b86bb6c883 add: добавлены "отслеживания" событий Яндекс.Метрики (только для согласившихся с отслеживанием) 2026-09-19 14:12:15 +03:00
erjemin 110c525e40 add: добавлен Яндеккс.Метрика 2026-09-19 13:34:39 +03:00
erjemin 9936be03ad fix: что-то не то с хидером. Попытка починить...
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 30s
2026-09-16 02:12:36 +03:00
erjemin afb75b7cf7 mod: готово для Google Tag Manager 2026-09-16 01:43:40 +03:00
erjemin abd4678e4a fix: корректные пути для favicon в html-ошибок и собранный под проект TailWind
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 28s
2026-09-16 00:48:04 +03:00
erjemin 978e5116f2 add: продпрод
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 1m27s
2026-09-16 00:10:37 +03:00
erjemin 74161c415d add: OG-картинка для твиттер и соцсетей 2026-09-16 00:03:26 +03:00
erjemin 9c3dd53f41 add: манифест тестового развертывания 2026-09-15 23:20:22 +03:00
erjemin 4d5e4cf86d add: шаблон для ошибки (3) 2026-09-14 23:44:16 +03:00
erjemin 1c2d0de14c add: шаблон для ошибки (2) 404 2026-09-14 20:56:43 +03:00
erjemin 2e11b536cb add: шаблон для ошибки (1) 404 2026-09-14 00:02:45 +03:00
erjemin c7ed8235ed minor: развертывание (2) - комментарии 2026-09-13 21:10:07 +03:00
erjemin 0773b25064 add: развертывание (1) - в dev-окружении все ок! 2026-09-13 21:08:08 +03:00
erjemin a729bcf9fb minor: design adjust (7) - update SVG icon 2026-09-13 01:42:42 +03:00
erjemin 49542cd49c minor: design adjust (6) - update breadcrumb navigation 2026-09-13 01:28:07 +03:00
erjemin 28556f0b12 minor: design adjust (5) 2026-09-13 00:08:44 +03:00
erjemin 8c682d3fef minor: design adjust (4) 2026-09-12 23:47:35 +03:00
erjemin 6122951614 minor: design adjust (2) 2026-09-12 22:52:43 +03:00
erjemin 5d0f2eb1b2 minor: design adjust (1) 2026-09-09 23:37:19 +03:00
erjemin f47b2ca364 add: бред, который жалко выбрасывать (1) 2026-09-09 00:23:55 +03:00
erjemin ab9b720326 add: блоги (4) верстка "Политика безопасности и конфиденциальности (SECURITY)" {'slug': 'security-and-privacy-policy'} 2026-09-09 00:23:09 +03:00
erjemin 0d0e901404 add: блоги (3) стили и шаблоны 2026-09-09 00:21:33 +03:00
erjemin 0c4ad38f84 minor: tmp-верстка (1) Соглашения о Конфидициальности (2) 2026-09-08 17:39:54 +03:00
erjemin fff98b3ebf minor: модели и админка (20) codemirror c html в заголовках 2026-09-08 17:38:53 +03:00
53 changed files with 9225 additions and 1779 deletions
+51
View File
@@ -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
View File
@@ -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
+73
View File
@@ -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
View File
@@ -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"]
+1
View File
@@ -583,6 +583,7 @@ CLI больше не нужен.
| `.env.sample` | Env template | | `.env.sample` | Env template |
| `Dockerfile` | Сборка | | `Dockerfile` | Сборка |
| `docker-compose.local.yml` | Dev | | `docker-compose.local.yml` | Dev |
| `docker-compose.prod-test.yml` | Тестовый Prod |
| `docker-compose.prod.yml` | Prod | | `docker-compose.prod.yml` | Prod |
| `config/nginx/hypn0-app--external-nginx.conf` | Nginx | | `config/nginx/hypn0-app--external-nginx.conf` | Nginx |
| `.gitea/workflows/docker-publish.yaml` | CI/CD | | `.gitea/workflows/docker-publish.yaml` | CI/CD |
+160
View File
@@ -0,0 +1,160 @@
# 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; # Разрешаем загрузку не слишком больших картинок
# --- СЕТЕВЫЕ ОПТИМИЗАЦИИ И ЗАЩИТА ОТ ТАЙМАУТОВ ЧЕРЕЗ VPN (MTU/MSS) ---
tcp_nopush on; # Отправляет заголовки и начало файла в одном TCP-пакете
tcp_nodelay on; # Отключает задержку Nagle для быстрых интерактивных ответов
ssl_buffer_size 4k; # Уменьшенный буфер TLS: решает зависания пакетов в VPN/WireGuard туннелях
# Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для 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 256; # Сжимать ответы от 256 байт
# Типы файлов для сжатия (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;
# --- БУФЕРИЗАЦИЯ В ПАМЯТИ (RAM) ---
# Позволяет отдавать большие страницы (80+ КБ) целиком из RAM без записи во временные файлы /var/lib/nginx/proxy
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 8 256k;
proxy_busy_buffers_size 256k;
proxy_temp_file_write_size 256k;
# Тайм-ауты (важно для долгих операций, если они есть)
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; # Всегда редиректим на основной домен
# }
+69
View File
@@ -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: ...
+174
View File
@@ -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"
+174
View File
@@ -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"
+21 -10
View File
@@ -5,18 +5,18 @@
## Что здесь лежит ## Что здесь лежит
### `tailwind/` — сборка Tailwind CSS v3.4 ### `tailwind/` — сборка Tailwind CSS v4
``` ```
tailwind/ tailwind/
├── package.json # Зависимости: tailwindcss@3.4, postcss, autoprefixer ├── package.json # Зависимости: @tailwindcss/cli, tailwindcss v4
├── package-lock.json # Фиксация версий ├── package-lock.json # Фиксация версий
└── build-tailwind.sh # ← запускается из корня проекта └── build-tailwind.sh # ← запускается из корня проекта (scripts/build-tailwind.sh)
``` ```
**Что делает:** **Что делает:**
- `build-tailwind.sh` создаёт временные файлы (`tailwind.config.js`, `postcss.config.js`, `input.css`), - `build-tailwind.sh` создаёт временный `input.css` с директивами `@import "tailwindcss";`, `@source` и импортом `tailwind-custom.css`,
запускает `npm ci` + `npm run build`, собирает `public/static/css/tailwind.min.css`, запускает `npm install` + `npm run build` (`@tailwindcss/cli`), собирает `public/static/css/tailwind.min.css`,
затем удаляет временные файлы. затем удаляет временные файлы.
- Результат: `public/static/css/tailwind.min.css` — минифицированный CSS со всеми - Результат: `public/static/css/tailwind.min.css` — минифицированный CSS со всеми
используемыми утилитами + кастомные стили из `hypn0/templates/css/tailwind-custom.css`. используемыми утилитами + кастомные стили из `hypn0/templates/css/tailwind-custom.css`.
@@ -37,6 +37,18 @@ alpine/
└── package-lock.json └── package-lock.json
``` ```
### `codemirror/` — сборка CodeMirror 6 для админки Django
```
codemirror/
├── package.json # Зависимости: @codemirror/*, esbuild, @uiw/codemirror-theme-solarized
└── package-lock.json # Фиксация версий
```
**Что делает:**
- `build-codemirror.sh` генерирует точку входа `src/editor.js` с поддержкой языков (HTML, CSS, JavaScript, JSON), автоматической смены темы (Solarized Light / Dark), форматирования и двусторонней синхронизации с Django админкой (`textarea[data-codemirror-editor]`).
- Собирает единый минифицированный IIFE-бандл через `esbuild` в `public/static/codemirror/editor.js`.
## Как запускать сборки ## Как запускать сборки
Из корня проекта: Из корня проекта:
@@ -50,18 +62,17 @@ bash scripts/build-htmx.sh
# Alpine.js # Alpine.js
bash scripts/build-alpine.sh bash scripts/build-alpine.sh
# CodeMirror 6 (для админки Django)
bash scripts/build-codemirror.sh
``` ```
Каждый скрипт сам делает всё: Каждый скрипт сам делает всё:
1. проверяет наличие `npm`; 1. проверяет наличие `npm`;
2. создаёт временные файлы; 2. создаёт временные файлы;
3. устанавливает зависимости через `npm ci`; 3. устанавливает зависимости через `npm ci` (или `npm install`);
4. запускает `npm run build`; 4. запускает `npm run build`;
5. кладёт готовый бандл в `public/static/...`; 5. кладёт готовый бандл в `public/static/...`;
6. удаляет временные файлы. 6. удаляет временные файлы.
В рабочем дереве не остаётся мусора от сборки. В рабочем дереве не остаётся мусора от сборки.
## Кодовая статика
В будущем — возможно, сборка CodeMirror 6 для админки Django.
+1373 -1469
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -6,8 +6,7 @@
"build": "tailwindcss -i input.css -o ../../public/static/css/tailwind.min.css --minify" "build": "tailwindcss -i input.css -o ../../public/static/css/tailwind.min.css --minify"
}, },
"devDependencies": { "devDependencies": {
"tailwindcss": "^3.4.0", "@tailwindcss/cli": "^4.0.0",
"postcss": "^8.4.35", "tailwindcss": "^4.0.0"
"autoprefixer": "^10.4.17"
} }
} }
+3
View File
@@ -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 -2
View File
@@ -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,14 @@ 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) 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}),
# ]
+6 -6
View File
@@ -163,10 +163,10 @@ class TbHypn0ItemAdminForm(CodeMirrorFormMixin):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Конфигурируем поля для CodeMirror # Конфигурируем поля для CodeMirror
self.setup_codemirror_field('s_title', language='text', self.setup_codemirror_field('s_title', language='html',
css_class='codemirror-width-l codemirror-no-lines') css_class='codemirror-width-l')
self.setup_codemirror_field('s_promo_title', language='text', self.setup_codemirror_field('s_promo_title', language='html',
css_class='codemirror-width-l codemirror-no-lines') css_class='codemirror-width-l')
self.setup_codemirror_field('s_promo_url', language='text', self.setup_codemirror_field('s_promo_url', language='text',
css_class='codemirror-width-xl codemirror-no-lines') css_class='codemirror-width-xl codemirror-no-lines')
self.setup_codemirror_field('i_likes_count', language='text', self.setup_codemirror_field('i_likes_count', language='text',
@@ -364,8 +364,8 @@ class BlogPostAdminForm(CodeMirrorFormMixin):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Конфигурируем поля для CodeMirror # Конфигурируем поля для CodeMirror
self.setup_codemirror_field('s_title', language='text', self.setup_codemirror_field('s_title', language='html',
css_class='codemirror-width-l codemirror-no-lines') css_class='codemirror-width-l')
self.setup_codemirror_field('slug', language='text', self.setup_codemirror_field('slug', language='text',
css_class='codemirror-width-l codemirror-no-lines') css_class='codemirror-width-l codemirror-no-lines')
self.setup_codemirror_field('i_order', language='text', self.setup_codemirror_field('i_order', language='text',
+16
View File
@@ -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)]
+4 -2
View File
@@ -20,7 +20,7 @@ HYPNO_ADJECTIVES = [
"потусторонний", "спиральный", "зыбкий", "пахучий", "скользкий", "туманный", "онейрический", "эфирный", "потусторонний", "спиральный", "зыбкий", "пахучий", "скользкий", "туманный", "онейрический", "эфирный",
"трансовый", "квантовый", "сочный", "гипно-абсурдный", "психоактивный", "сюрреалистичный", "магический", "трансовый", "квантовый", "сочный", "гипно-абсурдный", "психоактивный", "сюрреалистичный", "магический",
"синтетический", "хроматический", "перцептивный", "психоделичный", "галлюцинаторный", "трансцендентальный", "синтетический", "хроматический", "перцептивный", "психоделичный", "галлюцинаторный", "трансцендентальный",
"перфорированный", "перфорированный", "перцепторный", "гипно-иллюзорный", "трансцендентальный", "вульгарный",
] ]
HYPNO_NOUNS = [ HYPNO_NOUNS = [
@@ -30,12 +30,14 @@ HYPNO_NOUNS = [
"взмах", "взгляд", "морок", "туман", "свет", "кристалл", "поток", "сингулятор", "снег", "пульс", "глюк", "взмах", "взгляд", "морок", "туман", "свет", "кристалл", "поток", "сингулятор", "снег", "пульс", "глюк",
"качан", "сигнал", "поток", "свет", "синтез", "рык", "трепет", "плюх", "Му", "бip-бip", "карачун", "фронтир", "качан", "сигнал", "поток", "свет", "синтез", "рык", "трепет", "плюх", "Му", "бip-бip", "карачун", "фронтир",
"сторож", "привратник", "шёпот", "шлепок по&nbsp;жопе", "гриб", "плевок", "червяк", "пузырь", "психо-паук", "сторож", "привратник", "шёпот", "шлепок по&nbsp;жопе", "гриб", "плевок", "червяк", "пузырь", "психо-паук",
"перцептор",
] ]
HYPNO_GENITIVES = [ HYPNO_GENITIVES = [
"сознания", "Гипножабы", "бессознательного", "пустоты", "матрицы", "психо-паутины", "слизня", "ноосферы", "сознания", "Гипножабы", "бессознательного", "пустоты", "матрицы", "психо-паутины", "слизня", "ноосферы",
"нео-модерна", "транса", "глубины", "эфира", "плесени", "измерения", "пуантилизма", "субстанции", "нео-модерна", "транса", "глубины", "эфира", "плесени", "измерения", "пуантилизма", "субстанции",
"неопределенности", "континуума", "кислоты", "сына подруги твоей мамы" "неопределенности", "континуума", "кислоты", "сына подруги твоей мамы", "перцепции", "сновидений",
"галлюцинаций", "психоделики", "трансцендентности", "потустороннего мира",
] ]
HYPNO_POSTFIXES = [ HYPNO_POSTFIXES = [
+99 -4
View File
@@ -878,9 +878,9 @@ class CardBgStyleTests(BaseMediaTestCase):
self.assertIn('id="test-svg"', item.card_svg) self.assertIn('id="test-svg"', item.card_svg)
response = self.client.get(reverse("hypn0_site:index")) response = self.client.get(reverse("hypn0_site:index"))
self.assertContains(response, 'template shadowrootmode="open"') self.assertContains(response, "hypn0LazySvg")
self.assertContains(response, "hypn0-card-svg") self.assertContains(response, "hypn0-card-svg")
self.assertContains(response, "--hypn0-play: running !important") self.assertContains(response, item.file_svg.url)
class GalleryArchiveTests(BaseMediaTestCase): class GalleryArchiveTests(BaseMediaTestCase):
@@ -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, "Гипноузел не&nbsp;обнаружен", status_code=404)
self.assertContains(response, "СБОЙ МАТРИЦЫ // КОД?&nbsp;404", status_code=404)
self.assertContains(response, "return-btn", status_code=404)
self.assertContains(response, "Стираем остатки несущес&shy;твующей страницы", 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, "Гипноузел не&nbsp;обнаружен"),
("413", 413, "Перегрузка сенсорных каналов"),
("429", 429, "Слишком много мыслей в&nbsp;секунду"),
("500", 500, "Критический перегрев неокортекса"),
("502", 502, "Контейнер потерял сознание"),
("503", 503, "Плановый сеанс гипнотерапии"),
("504", 504, "Разрыв астральной связи"),
("under_reconstruction", 200, "Отдел мозга на&nbsp;реконструкции"),
]
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, "Гипноузел не&nbsp;обнаружен", 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("Гипноузел не&nbsp;обнаружен", 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"))
+6 -1
View File
@@ -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"),
]
+114
View File
@@ -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,
} }
+22 -34
View File
@@ -101,48 +101,36 @@ 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/hypn0.js' %}" defer></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>
document.addEventListener('htmx:configRequest', function(evt) {
var token = '{{ csrf_token }}';
if (!token) {
var match = document.cookie.match(/csrftoken=([^;]+)/);
if (match) token = match[1];
}
if (token) {
evt.detail.headers['X-CSRFToken'] = token;
}
});
function attachShadowRoots(root) {
(root || document).querySelectorAll('template[shadowrootmode]').forEach(function(tmpl) {
if (!tmpl.parentElement.shadowRoot) {
var mode = tmpl.getAttribute('shadowrootmode') || 'open';
var shadow = tmpl.parentElement.attachShadow({ mode: mode });
shadow.appendChild(tmpl.content.cloneNode(true));
tmpl.remove();
}
});
}
document.addEventListener('DOMContentLoaded', function() { attachShadowRoots(document); });
document.addEventListener('htmx:afterSwap', function(evt) { attachShadowRoots(evt.detail.target); });
</script>
{% block EXTRA_HEAD %}{% endblock EXTRA_HEAD %} {% block EXTRA_HEAD %}{% endblock EXTRA_HEAD %}
</head> </head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="bg-stone-50 text-emerald-700 dark:bg-zinc-950 dark:text-emerald-100 font-mono">{% block ADD_CSS1 %}{% endblock %}{% block ADD_CSS2 %}{% endblock %}{% block ADD_CSS3 %}{% endblock %}{% block BODY %} <body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="bg-stone-50 text-emerald-700 dark:bg-zinc-950 dark:text-emerald-100 font-mono">
<noscript>
<div class="sticky top-0 z-50 bg-amber-500 text-stone-950 px-4 py-2.5 text-center text-xs sm:text-sm font-bold shadow-lg border-b border-amber-600 flex items-center justify-center gap-2">
<span>⚠️</span>
<span>Для подчинения разума и генерации гипнотических SVG-матриц требуется включить <strong>JavaScript</strong> в вашем браузере!</span>
</div>
</noscript>
{% block ADD_CSS1 %}{% endblock %}{% block ADD_CSS2 %}{% endblock %}{% block ADD_CSS3 %}{% endblock %}{% block BODY %}
{% include "block/header.html" %} {% include "block/header.html" %}
<main id="main-content" class="mx-auto min-h-screen max-w-7xl px-4 py-8"> <main id="main-content" class="mx-auto min-h-screen max-w-7xl px-4 py-8">
{% block CONTENT %}{% endblock CONTENT %} {% block CONTENT %}{% endblock CONTENT %}
</main> </main>
{# {% include "blocks/footer.jinja2" %} #}{% endblock BODY %}{% if not ALLOW_TRACKING %}{# Если клиент еще не согласился на отслеживание покажем ему уведомление #}{% include "block/allow-tracking.html" %}{% else %}{# Если согласился -- отслеживаем#} {# {% include "blocks/footer.jinja2" %} #}{% endblock BODY %}{% if not ALLOW_TRACKING %}{# Если клиент еще не согласился на отслеживание покажем ему уведомление #}{% include "block/allow-tracking.html" %}{% else %}{# Если согласился -- отслеживаем#}
<script src="{% static 'js/analytics.js' %}"></script>{% endif %} <script src="{% static 'js/analytics.js' %}"></script>
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5FVH3KMJ" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<noscript><div><img src="https://mc.yandex.ru/watch/112812760" style="position:absolute; left:-9999px;" alt="" /></div></noscript>{% endif %}
{% block ADD_JS1 %}{% endblock ADD_JS1 %}{% block ADD_JS2 %}{% endblock ADD_JS2 %}{% block ADD_JS3 %}{% endblock ADD_JS3 %}</body> {% block ADD_JS1 %}{% endblock ADD_JS1 %}{% block ADD_JS2 %}{% endblock ADD_JS2 %}{% block ADD_JS3 %}{% endblock ADD_JS3 %}</body>
</html> </html>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">СБОЙ МАТРИЦЫ // КОД&nbsp;400</div>
<h1 class="error-title">Сбой перцепционного протокола</h1>
<p class="error-desc">
Клиент искажает ментальные сигналы или имеет невалидную структуру биоритмов мозговых данных. Неокортекс сервера отверг запрос из-за&nbsp;синтаксического&nbsp;диссонанса.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Некорректно сформулированная мысль разрушает гармонию халфтона. Перепроверьте параметры запроса перед&nbsp;повторным сеансом внушения.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Сбросить параметры и&nbsp;вернуться</span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
<span>← На&nbsp;шаг назад</span>
</button>
</div>
<!-- Панель калибровки с задержкой и смешными фразами -->
<div class="calibration-panel" id="calibration-panel">
<div class="calibration-status" id="calibration-status">
Инициа&shy;лизация очистки буфера…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Декодируем искажённые байты входящего сигнала&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Бюрократ 24-го уровня отклоняет невалидный пакет&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Очищаем буфер неокортекса от&nbsp;ментального шума&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Протокол сброшен! Возвращаемся на&nbsp;главную&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">ОГРАНИЧЕНИЕ ДОСТУПА // КОД&nbsp;401</div>
<h1 class="error-title">Требуется ментальная идентификация</h1>
<p class="error-desc">
Вы попытались проникнуть в&nbsp;закрытый сектор подсознания без&nbsp;действующего пропуска или аутентификационного чипа.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Свободная ментальность и&nbsp;анонимные мысли не&nbsp;допускаются к&nbsp;генераторам высшего порядка. Предъявите биометрический слепок или вернитесь в&nbsp;общий сектор.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Вернуться в&nbsp;общий сектор</span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
<span>← На&nbsp;шаг назад</span>
</button>
</div>
<!-- Панель калибровки с задержкой и смешными фразами -->
<div class="calibration-panel" id="calibration-panel">
<div class="calibration-status" id="calibration-status">
Инициа&shy;лизация сканирования биометрии…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Сканируем ментальные отпечатки и&nbsp;токены доступа&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Проверяем наличие чипа мозгового слизня&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня фиксирует отсутствие пропуска&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Идентификация отклонена! Телепор&shy;тация в&nbsp;безопасную зону&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">ДОСТУП ЗАБЛОКИРОВАН // КОД&nbsp;403</div>
<h1 class="error-title">Ментальная блокировка</h1>
<p class="error-desc">
У&nbsp;вас нет достаточного уровня перцепционного допуска для&nbsp;просмотра этого сектора подсознания. Вердикт окончательный и&nbsp;обжалованию не&nbsp;подлежит.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Форма 38-Б о&nbsp;праве на&nbsp;любопытство не&nbsp;была заверена в&nbsp;Центральном Бюрократическом Управлении. Несанкционированное созерцание карается понижением до&nbsp;34-го уровня.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Принять запрет и&nbsp;вернуться</span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
<span>← На&nbsp;шаг назад</span>
</button>
</div>
<!-- Панель калибровки с задержкой и смешными фразами -->
<div class="calibration-panel" id="calibration-panel">
<div class="calibration-status" id="calibration-status">
Инициа&shy;лизация проверки гербовых печатей…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Проверяем гербовые печати и&nbsp;визы Центрального Бюро&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Выписываем штраф за&nbsp;несанкционированное любопытство&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Блокируем блоки краткосрочной памяти и&nbsp;транспорт в&nbsp;гипокамп&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Протокол закрыт! Перенаправ&shy;ление на&nbsp;главную&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">СБОЙ МАТРИЦЫ // КОД?&nbsp;404</div>
<h1 class="error-title">Гипноузел не&nbsp;обнаружен</h1>
<p class="error-desc">
Запра&shy;шиваемый блок ментального пространства стёрт из&nbsp;подсознания сервера или никогда не&nbsp;существовал в&nbsp;этой версии&nbsp;вселенной.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Попытка осознать отсутс&shy;твующую координату приводит к&nbsp;локальному перегреву неокортекса. Не&nbsp;вгляды&shy;вайтесь в&nbsp;пустоту. Гипножаба рекомендует немедленно сменить вектор&nbsp;внимания.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Подчиниться и&nbsp;вернуться на&nbsp;главную</span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
<span>← На&nbsp;шаг назад</span>
</button>
</div>
<!-- Панель калибровки с задержкой и смешными фразами -->
<div class="calibration-panel" id="calibration-panel">
<div class="calibration-status" id="calibration-status">
Инициа&shy;лизация перека&shy;либровки сознания…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Стираем остатки несущес&shy;твующей страницы из&nbsp;подсознания&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Бюрократ 24-го уровня заверяет протокол&nbsp;404&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Синхронизируем частоту мерцания с&nbsp;перцеп&shy;торным полем&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Внушение принято! Телепор&shy;тация на&nbsp;главную&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">ПРЕВЫШЕН ЛИМИТ // КОД&nbsp;413</div>
<h1 class="error-title">Перегрузка сенсорных каналов</h1>
<p class="error-desc">
Загружаемое изображение оказалось слишком массивным для&nbsp;восприятия ментальной матрицей. Сервер перцепций отказался поглощать такой объём&nbsp;байтов.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Попытка скормить гигабайты растра хрупкому SVG-генератору расценивается как&nbsp;диверсия. Уменьшите разрешение или сожмите файл перед&nbsp;повторной загрузкой в&nbsp;ментальную&nbsp;матрицу.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Сжать восприятие и&nbsp;вернуться</span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
<span>← На&nbsp;шаг назад</span>
</button>
</div>
<!-- Панель калибровки с задержкой и смешными фразами -->
<div class="calibration-panel" id="calibration-panel">
<div class="calibration-status" id="calibration-status">
Инициа&shy;лизация сброса переполненного буфера…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Сбрасываем перегруженный ментальный буфер входного потока&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Охлаждаем приемные каналы Nginx жидким азотом&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня успокаивает перцепционные волны ментальности&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Буфер чист! Возвращаемся к&nbsp;генератору&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">ЗАЩИТА ОТ СПАМА // КОД&nbsp;429</div>
<h1 class="error-title">Слишком много мыслей в&nbsp;секунду</h1>
<p class="error-desc">
Вы отправляете импульсы быстрее, чем&nbsp;Гипножаба успевает перемигиваться. Включена защита от&nbsp;ментального спама и&nbsp;перегрева процессоров.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Перцепционный генератор требует расслабленного состояния мышления, а&nbsp;не&nbsp;ментальных судорог и&nbsp;урагана кликов. Сделайте глубокий вдох, сосчитайте до&nbsp;сорока двух и&nbsp;позвольте трансу стабилизироваться.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Остудить пыл и&nbsp;вернуться</span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
<span>← На&nbsp;шаг назад</span>
</button>
</div>
<!-- Панель калибровки с задержкой и смешными фразами -->
<div class="calibration-panel" id="calibration-panel">
<div class="calibration-status" id="calibration-status">
Инициа&shy;лизация медитативного охлаждения…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Замедляем частоту синаптических импульсов&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Восстанавливаем квоту запросов в&nbsp;реестре Гипножабы&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Приводим сознание в&nbsp;состояние медитативного покоя&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Лимиты восстановлены! Возвращаемся на&nbsp;главную&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">АВАРИЯ СЕРВЕРА // КОД&nbsp;500</div>
<h1 class="error-title">Нейроклинч интроспекций</h1>
<p class="error-desc">
Внутренний обработчик Т-волн подсознания столкнулся с&nbsp;непредвиденным логическим парадоксом. Ментальные службы уже приведены в&nbsp;состояние боевого транса.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Стек вызовов испустил синий дым, но&nbsp;паника строго запрещена циркуляром&nbsp;№16. Гипножаба, Мозгоклюй и&nbsp;дежурные Бри-Гады мозговых слизней уже&nbsp;инспектируют упавший поток перцепции и&nbsp;устраняет ментальную деградацию.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Дождаться регенерации и&nbsp;вернуться</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">
Инициа&shy;лизация аварийной регенерации ядра…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Локализуем утечку астральной памяти в&nbsp;ядре сервера&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Бюрократ 24-го уровня перезапускает упавший процесс&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Гипножаба восстанавливает гармонию матриц неокотекса&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Ядро стабилизировано! Телепор&shy;тация на&nbsp;главную&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">ОБРЫВ ШЛЮЗА // КОД&nbsp;502</div>
<h1 class="error-title">Нет связи с&nbsp;Гипокампом</h1>
<p class="error-desc">
Прокси-сервер Nginx послал импульс через энторинальную кору в&nbsp;Django-гипокамп, но&nbsp;обратный синапсический отклик потерян. Аксоны в&nbsp;состоянии звенящей астральной&nbsp;пустоты.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Gunicorn ушёл в&nbsp;глубокую интроспекцию или временно дезориентирован в&nbsp;docker-пространстве. Бри-Гады мозговых слизней уже&nbsp;инспектируют упавший поток перцепции и&nbsp;устраняет ментальную деградацию.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Восстановить связь и&nbsp;вернуться</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">
Инициа&shy;лизация реанимации шлюза…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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-сокеты&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Подаем разряд бodрящих волн Шумана в&nbsp;воркеры Gunicorn&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Прокси-шлюз Nginx восстанавливает ментальный маршрут&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Мост налажен! Возвращаемся на&nbsp;главную&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">ТЕХОБСЛУЖИВАНИЕ // КОД&nbsp;503</div>
<h1 class="error-title">Плановый сеанс гипнотерапии</h1>
<p class="error-desc">
Сервер временно закрыт на&nbsp;калибровку ментальных матриц или испытывает запредельный наплыв адептов Гипножабы.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Проводятся регламентные работы по&nbsp;смазке осей гипнотических нейроспиралей и&nbsp;продувке камер интроспекций халфтона. Задействованы усиленные технические Бри-Гады мозговых слизней под&nbsp;надзором Мозгоклюя. Скоро сервис вернётся в&nbsp;строй.</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">
Инициа&shy;лизация технической диагностики…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Калибруем точность алгоритмов смыслового мерцания&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Смазываем оси гипнотических спиралей&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня завершает технический аудит&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Профилактика завершена без&nbsp;подписания акта о&nbsp;выполнении работ! Телепор&shy;тация на&nbsp;главную&hellip;" }
];
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>
+481
View File
@@ -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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">ТАЙМАУТ ШЛЮЗА // КОД&nbsp;504</div>
<h1 class="error-title">Разрыв астральной связи</h1>
<p class="error-desc">
Вышестоящий перцептивный узел в&nbsp;состоянии рекурсивной интроспекции и&nbsp;не успевает ответить в&nbsp;отведённый квант&nbsp;времени.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Время&nbsp;— понятие относительное, но&nbsp;таймаут Nginx абсолютен. Ментальный транс осознанных сновидений думателя прерван во&nbsp;избежание <nobr>зацикливания нейро-континуума.</nobr></em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Восстановить таймлайн и&nbsp;вернуться</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">
Инициа&shy;лизация синхронизации таймлайна…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Прерываем бесконечные квантовые вычисления&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Перезапускаем таймеры ожидания шлюза Nginx&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Синхронизируем ментальное состояние с&nbsp;сервером Гипножабы&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Связь восстановлена! Возвращаемся на&nbsp;главную&hellip;" }
];
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="/media/_error/favicon.svg" />
<link rel="shortcut icon" href="/media/_error/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">Отдел мозга на&nbsp;реконструкции</h1>
<p class="error-desc">
Узел ментальной матрицы перестраивается по&nbsp;бредо-чертежам Гипножабы. Бри-Гады мозговых слизней под&nbsp;контролем Мозгоклюя разворачивают новый слой интроспекций. Скоро здесь появится нечто гипнотически непознаваемое.
</p>
<div class="hypno-quote">
<big>Справка бюрократа 24-го уровня:</big><br/>&nbsp;<br/>
<span style="margin-left: -0.49em;">«</span><em>Ведутся строительно-монтажные работы по&nbsp;укладке новых векторных силовых линий. Проход без&nbsp;мантальной каски и&nbsp;без вживленного мозгового слизня ограничен даже гражданам в&nbsp;глубоком&nbsp;трансе.</em>»
</div>
<div class="actions-box">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="return-btn">
<span>● Попытка вынырнуть из&nbsp;интроспекции</span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.history.length > 1 ? window.history.back() : window.location.href='/'">
<span>← На&nbsp;шаг назад</span>
</button>
</div>
<!-- Панель калибровки с задержкой и смешными фразами -->
<div class="calibration-panel" id="calibration-panel">
<div class="calibration-status" id="calibration-status">
Инициа&shy;лизация перемещения из зоны работ…
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<div class="calibration-subtext">
<span>Снижение ментальной нагрузки на&nbsp;кластер</span>
<a href="/" class="instant-link" id="skip-link">Пройти гипноз мгновенно&nbsp;→</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: Закладываем квантовый фундамент новой функциональности&hellip;" },
{ at: 25, text: "✦ Фаза 2/4: Монтируем передовые генераторы векторных паттернов&hellip;" },
{ at: 55, text: "✦ Фаза 3/4: Бюрократ 24-го уровня согласует счет-фактуру&hellip;" },
{ at: 85, text: "✦ Фаза 4/4: Акт согласования готов! Это не поможет, но возвращаемся в&nbsp;активную зону сайта&hellip;" }
];
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>
+15 -10
View File
@@ -1,13 +1,18 @@
{# НИЖНИЙ POPUP О КУКУХ И ОТСЛЕЖИВАНИИ #}<div id="allow-tracking-popup" x-data="{ agreed: !!localStorage.getItem('hypn0_vid') }" x-show="!agreed" x-cloak class="sticky bottom-0 z-50 bg-gray-100/80 dark:bg-slate-800/70 backdrop-blur border-t border-gray-300 dark:border-gray-700"> {# НИЖНИЙ POPUP О КУКУХ И ОТСЛЕЖИВАНИИ #}<div id="allow-tracking-popup" x-data="{ agreed: !!localStorage.getItem('hypn0_vid') }" x-show="!agreed" x-cloak class="sticky bottom-0 z-50 bg-gray-100/95 dark:bg-slate-800/90 backdrop-blur border-t border-gray-300 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 py-8 grid grid-cols-[auto_1fr] gap-x-4"> <div class="max-w-7xl mx-auto p-3 sm:p-4 md:px-4 md:py-8 flex flex-col md:grid md:grid-cols-[1fr_auto] gap-2.5 md:gap-x-4 items-center">
<p class="flex-col px-6 py-6"> <p class="p-0 md:px-6 md:py-6 text-xs sm:text-sm md:text-base leading-snug md:leading-normal">
Внимание! В&nbsp;соответствии с&nbsp;152-<abbr title="Федеральный закон">ФЗ</abbr> <span class="md:hidden">
и&nbsp;<abbr title="General Data Protection Regulation">GDPR</abbr> и&nbsp;протоколами Нового Нью-Йорка, Внимание! Этот сайт использует файлы-присоски (cookies) и алгоритмы ментального мерцания в&nbsp;соответствии с&nbsp;152-<abbr title="Федеральный закон">ФЗ</abbr>, <abbr title="General Data Protection Regulation">GDPR</abbr> и&nbsp;<a href="/blog/security-and-privacy-policy">политикой конфи&shy;денциаль&shy;ности</a>.
этот сайт использует файлы-присоски (cookies). ВСЕ СЛАВЯТ ГИПНОЖАБУ.&nbsp;Ваши обезли&shy;ченные данные </span>
считываются алгоритмами ментального мерцания. Оставаясь на&nbsp;гипно-платформе <strong>hypn0</strong>, вы&nbsp;добровольно <span class="hidden md:inline">
впадаете в&nbsp;транс и&nbsp;принимаете условия нашей <a href="/info/privacy-policy">политики Внимание! В&nbsp;соответствии с&nbsp;152-<abbr title="Федеральный закон">ФЗ</abbr>
коконфи&shy;денциаль&shy;ности</a>. <strong>Не&nbsp;пытайтесь переключить вкладку!</strong> и&nbsp;<abbr title="General Data Protection Regulation">GDPR</abbr> и&nbsp;протоколами Нового Нью-Йорка,
этот сайт использует файлы-присоски (cookies). ВСЕ СЛАВЯТ ГИПНОЖАБУ.&nbsp;Ваши обезли&shy;ченные данные
считываются алгоритмами ментального мерцания. Оставаясь на&nbsp;гипно-платформе <strong>hypn0</strong>, вы&nbsp;добровольно
впадаете в&nbsp;транс и&nbsp;принимаете условия нашей <a href="/blog/security-and-privacy-policy">политики
конфи&shy;денциаль&shy;ности</a>. <strong>Не&nbsp;пытайтесь переключить вкладку!</strong>
</span>
</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="w-full md:w-auto text-xs sm:text-sm md:text-lg px-4 md:px-6 py-2 md:py-2 my-0 md:my-6 border md:border-2 rounded-md bg-emerald-600 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 text-white font-bold whitespace-nowrap cursor-pointer transition-colors">Подчиниться Гипножабе!</button>
</div> </div>
</div>{# / НИЖНИЙ POPUP О КУКУХ И ОТСЛЕЖИВАНИИ #} </div>{# / НИЖНИЙ POPUP О КУКУХ И ОТСЛЕЖИВАНИИ #}
+8 -26
View File
@@ -8,32 +8,14 @@
class="absolute inset-0 z-10 flex items-center justify-center p-3 sm:p-4 border-0 border-none no-underline hover:border-0 hover:border-none focus:outline-none" class="absolute inset-0 z-10 flex items-center justify-center p-3 sm:p-4 border-0 border-none no-underline hover:border-0 hover:border-none focus:outline-none"
title="«{{ item.s_title }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG" title="«{{ item.s_title }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG"
aria-label="«{{ item.s_title }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG"> aria-label="«{{ item.s_title }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG">
<div class="hypn0-card-svg w-full h-full flex items-center justify-center drop-shadow-sm transition-transform duration-300 group-hover:scale-105 border-0"> <div x-data="hypn0LazySvg('{% if item.file_svg %}{{ item.file_svg.url }}{% endif %}')"
<template shadowrootmode="open"> class="hypn0-card-svg w-full h-full flex items-center justify-center drop-shadow-sm transition-transform duration-300 group-hover:scale-105 border-0">
{# Declarative Shadow DOM: изолирует ID (<defs id="s1"..>) и стили каждой картины в галерее. #} <noscript>
{# В отличие от <img>, события мыши не блокируются, а в отличие от обычного inline SVG нет конфликтов идентификаторов. #} <div class="flex flex-col items-center justify-center p-3 text-center gap-1">
<style> <span class="text-[11px] font-bold text-amber-600 dark:text-amber-400">Требуется JS для анимации</span>
:host { <span class="text-[10px] text-stone-500 dark:text-stone-400 underline">«{{ item.s_title }}»</span>
display: flex; </div>
width: 100%; </noscript>
height: 100%;
align-items: center;
justify-content: center;
pointer-events: auto;
}
svg {
width: 100%;
height: 100%;
object-fit: contain;
}
{# В покое внутри SVG действует --hypn0-play: paused (0% нагрузки на CPU). #}
{# При наведении мыши на хост-карточку переопределяем переменную на running, и анимация оживает. #}
:host(:hover) svg, svg:hover {
--hypn0-play: running !important;
}
</style>
{{ item.card_svg|safe }}
</template>
</div> </div>
</a> </a>
+6 -6
View File
@@ -5,13 +5,13 @@
<div class="max-w-7xl mx-auto px-2 grid grid-cols-[1fr_auto] gap-x-4"> <div class="max-w-7xl mx-auto px-2 grid grid-cols-[1fr_auto] gap-x-4">
{# ЛОГОТИП #}{% comment %} {# ЛОГОТИП #}{% comment %}
border-0 — логотип оборачивается в ссылку без текста, отключаем типовое border-0 — логотип оборачивается в ссылку без текста, отключаем типовое
пунктирное подчёркивание ссылки, которое иначе появится под картинкой.{% endcomment %}<div class="flex py-4"> пунктирное подчёркивание ссылки, которое иначе появится под картинкой.{% endcomment %}<div class="flex items-center py-2 md:py-4">
<a href="/" class="border-0" title="HypnoSVG (hypn0) — Главная"><img src="{% static 'img/logo-hypn0.svg' %}" alt="HypnoSVG (hypn0) — Генератор анимированных SVG-халфтонов" title="HypnoSVG (hypn0)"></a> <a href="/" class="border-0 flex items-center" title="HypnoSVG (hypn0) — Главная"><img src="{% static 'img/logo-hypn0.svg' %}" alt="HypnoSVG (hypn0) — Генератор анимированных SVG-халфтонов" title="HypnoSVG (hypn0)" class="h-7 sm:h-8 md:h-12 w-auto"></a>
</div>{# /ЛОГОТИП #} </div>{# /ЛОГОТИП #}
{# ПРАВОЕ МЕНЮ #}<menu class="flex items-center gap-x-4 justify-self-start text-xl font-semibold text-slate-900 dark:text-slate-100"> {# ПРАВОЕ МЕНЮ #}<menu class="flex items-center gap-x-2 md:gap-x-4 justify-self-start text-sm sm:text-base md:text-xl font-semibold text-slate-900 dark:text-slate-100">
<a href="{% url 'hypn0_site:gallery_archive' %}" class="px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg border-0 text-slate-900 dark:text-slate-100">галерея</a> <a href="{% url 'hypn0_site:gallery_archive' %}" class="px-2.5 py-1 sm:px-4 sm:py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg border-0 text-slate-900 dark:text-slate-100">галерея</a>
<a href="{% url 'hypn0_site:blog_feed' %}" class="px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden md:block hyphens-none border-0 text-slate-900 dark:text-slate-100">блог</a> <a href="{% url 'hypn0_site:blog_feed' %}" class="px-2.5 py-1 sm:px-4 sm:py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden md:block hyphens-none border-0 text-slate-900 dark:text-slate-100">блог</a>
<a href="#" class="px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden lg:block border-0 text-slate-900 dark:text-slate-100" @click.prevent="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)" title="Переключить тему"><span x-text="darkMode?'●':'○'"></span></a> <a href="#" class="px-2.5 py-1 sm:px-4 sm:py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden lg:block border-0 text-slate-900 dark:text-slate-100" @click.prevent="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)" title="Светлая/Темная тема"><span x-text="darkMode?'●':'○'"></span></a>
</menu>{# /ПРАВОЕ МЕНЮ #} </menu>{# /ПРАВОЕ МЕНЮ #}
</div> </div>
</header> </header>
+2 -1
View File
@@ -41,7 +41,8 @@
<input type="hidden" name="color" value="{% if primary_color %}{{ primary_color }}{% else %}#a855ff{% endif %}"> <input type="hidden" name="color" value="{% if primary_color %}{{ primary_color }}{% else %}#a855ff{% endif %}">
<button id="like-to-gallery" type="submit" title="Отправить в галерею" <button id="like-to-gallery" type="submit" title="Отправить в галерею"
:disabled="isPublishing" :disabled="isPublishing"{% if ALLOW_TRACKING %}
{# Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'like_art')"{% endif %}
class="group flex items-center gap-2 bg-emerald-600/90 hover:bg-emerald-600 text-white text-xs font-bold uppercase tracking-wide backdrop-blur-md px-4 py-2.5 rounded-full shadow-xl transition-all hover:scale-105 active:scale-95 disabled:opacity-60 disabled:scale-100 disabled:pointer-events-none cursor-pointer"> class="group flex items-center gap-2 bg-emerald-600/90 hover:bg-emerald-600 text-white text-xs font-bold uppercase tracking-wide backdrop-blur-md px-4 py-2.5 rounded-full shadow-xl transition-all hover:scale-105 active:scale-95 disabled:opacity-60 disabled:scale-100 disabled:pointer-events-none cursor-pointer">
<template x-if="isPublishing"> <template x-if="isPublishing">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
+2 -1
View File
@@ -10,7 +10,8 @@
class="inline-flex items-center"> class="inline-flex items-center">
{% csrf_token %} {% csrf_token %}
<button type="submit" <button type="submit"
{% if user_voted %}disabled{% endif %} {% if user_voted %}disabled{% endif %}{% if ALLOW_TRACKING %}
{# Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'like_art')"{% endif %}
class="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-bold transition-all {% if user_voted %}bg-rose-500/20 text-rose-400 border border-rose-500/30 cursor-default{% else %}bg-stone-200 dark:bg-stone-800 hover:bg-rose-500 hover:text-white text-stone-700 dark:text-stone-300 cursor-pointer hover:scale-105 active:scale-95 shadow-sm{% endif %}"> class="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-bold transition-all {% if user_voted %}bg-rose-500/20 text-rose-400 border border-rose-500/30 cursor-default{% else %}bg-stone-200 dark:bg-stone-800 hover:bg-rose-500 hover:text-white text-stone-700 dark:text-stone-300 cursor-pointer hover:scale-105 active:scale-95 shadow-sm{% endif %}">
<svg class="w-4 h-4 {% if user_voted %}fill-rose-500 text-rose-500{% else %}fill-none stroke-current{% endif %}" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg class="w-4 h-4 {% if user_voted %}fill-rose-500 text-rose-500{% else %}fill-none stroke-current{% endif %}" viewBox="0 0 24 24" stroke="currentColor" 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"/> <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"/>
+382
View File
@@ -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 %}
+233
View File
@@ -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 %}
+385
View File
@@ -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;
}
+1 -1
View File
@@ -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">
+4 -3
View File
@@ -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">
@@ -123,7 +123,7 @@
<div class="flex flex-wrap items-center gap-2.5"> <div class="flex flex-wrap items-center gap-2.5">
<!-- Кнопка Копировать SVG --> <!-- Кнопка Копировать SVG -->
<button type="button" <button type="button"
@click="navigator.clipboard.writeText(document.getElementById('raw-svg-source').value); copiedSvg = true; setTimeout(() => copiedSvg = false, 2500)" @click="navigator.clipboard.writeText(document.getElementById('raw-svg-source').value); copiedSvg = true; setTimeout(() => copiedSvg = false, 2500){% if ALLOW_TRACKING %}; window.ym && ym(112812760, 'reachGoal', 'copy_svg'){% endif %}"
class="flex items-center gap-2 px-4 py-2 bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-800 dark:text-stone-200 rounded-xl text-xs font-bold transition-all hover:scale-105 active:scale-95 shadow-sm cursor-pointer"> class="flex items-center gap-2 px-4 py-2 bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-800 dark:text-stone-200 rounded-xl text-xs font-bold transition-all hover:scale-105 active:scale-95 shadow-sm cursor-pointer">
<svg x-show="!copiedSvg" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg x-show="!copiedSvg" 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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
@@ -136,7 +136,8 @@
<!-- Кнопка Скачать SVG --> <!-- Кнопка Скачать SVG -->
<a href="{% url 'hypn0_site:gallery_download' item.s_hash_id %}" <a href="{% url 'hypn0_site:gallery_download' item.s_hash_id %}"
download="hypn0-{{ item.s_hash_id }}.svg" download="hypn0-{{ item.s_hash_id }}.svg"{% if ALLOW_TRACKING %}
{# Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'download_svg')"{% endif %}
class="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-bold transition-all hover:scale-105 active:scale-95 shadow-md border-0 cursor-pointer"> class="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-bold transition-all hover:scale-105 active:scale-95 shadow-md border-0 cursor-pointer">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/> <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"/>
+41 -35
View File
@@ -2,7 +2,7 @@
{% block CONTENT %} {% block CONTENT %}
<div class="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-8 items-start" <div class="flex flex-col lg:grid lg:grid-cols-[400px_1fr] gap-6 lg:gap-8 items-start"
x-data="{ x-data="{
/* Активная вкладка: 'style' (фигуры), 'params' (ползунки), 'color' (палитра) */ /* Активная вкладка: 'style' (фигуры), 'params' (ползунки), 'color' (палитра) */
activeTab: 'style', activeTab: 'style',
@@ -47,6 +47,7 @@
/* Служебные состояния интерфейса */ /* Служебные состояния интерфейса */
isLoading: false, /* Идет ли процесс генерации SVG (индикация, блокировка) */ isLoading: false, /* Идет ли процесс генерации SVG (индикация, блокировка) */
hasChanges: true, /* Флаг наличия правок в форме для активности кнопки */ hasChanges: true, /* Флаг наличия правок в форме для активности кнопки */
hasGenerated: false, /* Флаг завершения хотя бы одной генерации */
hasImage: false, /* Флаг загрузки пользовательского файла */ hasImage: false, /* Флаг загрузки пользовательского файла */
fileName: '', /* Имя выбранного файла */ fileName: '', /* Имя выбранного файла */
imagePreviewUrl: null, /* Превью загруженного файла (Blob URL) */ imagePreviewUrl: null, /* Превью загруженного файла (Blob URL) */
@@ -106,7 +107,7 @@
}"> }">
<!-- ЛЕВАЯ ПАНЕЛЬ: НАСТРОЙКИ --> <!-- ЛЕВАЯ ПАНЕЛЬ: НАСТРОЙКИ -->
<aside class="space-y-6"> <aside class="contents lg:block lg:space-y-6">
<form hx-post="/generate" <form hx-post="/generate"
hx-target="#preview" hx-target="#preview"
hx-trigger="submit" hx-trigger="submit"
@@ -115,8 +116,8 @@
@change="hasChanges = true" @change="hasChanges = true"
@input="hasChanges = true" @input="hasChanges = true"
@htmx:before-request="startThinking()" @htmx:before-request="startThinking()"
@htmx:after-request="isLoading = false; hasChanges = false" @htmx:after-request="isLoading = false; hasChanges = false; hasGenerated = true"
class="relative bg-white dark:bg-zinc-900 p-6 rounded-2xl shadow-sm border border-stone-200 dark:border-zinc-800 overflow-hidden"> class="contents lg:block lg:relative lg:bg-white lg:dark:bg-zinc-900 lg:p-6 lg:rounded-2xl lg:shadow-sm lg:border lg:border-stone-200 lg:dark:border-zinc-800 lg:overflow-hidden">
{% csrf_token %} {% csrf_token %}
<!-- Фоновая спираль размышления при генерации (прижата снизу на всю левую панель) --> <!-- Фоновая спираль размышления при генерации (прижата снизу на всю левую панель) -->
@@ -127,37 +128,37 @@
x-transition:leave="transition ease-in duration-200" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95" x-transition:leave-end="opacity-0 scale-95"
class="absolute inset-0 z-20 pointer-events-none flex items-end justify-center overflow-hidden rounded-2xl bg-white/70 dark:bg-zinc-900/75 backdrop-blur-[2px]"> class="hidden lg:flex absolute inset-0 z-20 pointer-events-none items-end justify-center overflow-hidden rounded-2xl bg-white/70 dark:bg-zinc-900/75 backdrop-blur-[2px]">
<img src="{% static 'img/thinking.svg' %}" alt="thinking" <img src="{% static 'img/thinking.svg' %}" alt="thinking"
class="w-120 h-120 -mb-24 opacity-75 animate-spin [animation-duration:10s]"> class="w-120 h-120 -mb-24 opacity-75 animate-spin [animation-duration:10s]">
</div> </div>
<!-- Загрузка файла --> <!-- Загрузка файла -->
<div class="mb-8"> <div class="order-1 w-full bg-white dark:bg-zinc-900 p-3 sm:p-4 lg:p-0 rounded-2xl lg:rounded-none border border-stone-200 dark:border-zinc-800 lg:border-0 shadow-sm lg:shadow-none mb-0 lg:mb-8">
<label class="block text-sm font-bold mb-2 uppercase tracking-wider text-stone-500 dark:text-zinc-400">Источник</label> <label class="hidden lg:block text-sm font-bold mb-2 uppercase tracking-wider text-stone-500 dark:text-zinc-400">Источник</label>
<div class="relative group"> <div class="relative group">
<input type="file" name="image" accept="image/*" <input type="file" name="image" accept="image/*"
@change="handleFileChange($event)" @change="handleFileChange($event)"
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"> class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
<div <div
:class="hasImage ? 'border-emerald-500 bg-emerald-50/20 dark:bg-emerald-950/20' : 'border-stone-300 dark:border-zinc-700'" :class="hasImage ? 'border-emerald-500 bg-emerald-50/20 dark:bg-emerald-950/20' : 'border-stone-300 dark:border-zinc-700'"
class="border-2 border-dashed group-hover:border-emerald-500 rounded-xl p-6 flex flex-col items-center justify-center text-center transition-all"> class="border-2 border-dashed group-hover:border-emerald-500 rounded-xl p-3 sm:p-4 lg:p-6 flex flex-col items-center justify-center text-center transition-all">
<!-- Состояние 1: Файл не выбран --> <!-- Состояние 1: Файл не выбран -->
<div x-show="!hasImage" class="text-stone-400 group-hover:text-emerald-500 flex flex-col items-center gap-3 transition-colors"> <div x-show="!hasImage" class="text-stone-400 group-hover:text-emerald-500 flex flex-row lg:flex-col items-center gap-3 transition-colors">
<svg class="w-20 h-20 fill-current transition-colors opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <svg class="w-8 h-8 sm:w-10 sm:h-10 lg:w-20 lg:h-20 shrink-0 fill-current transition-colors opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M477 174c8.812-13.708 13.948-29.993 13.948-47.466 0-48.55-39.499-88.048-88.05-88.048-32.839 0-61.516 18.084-76.651 44.803-22.773-5.031-46.356-7.586-70.424-7.586-24.003 0-47.624 2.55-70.427 7.582-15.135-26.717-43.813-44.799-76.65-44.799-48.55 0-88.048 39.498-88.048 88.049 0 17.473 5.137 33.759 13.949 47.467C11.987 204.676 0 239.016 0 274.61c0 53.849 27.194 104.194 76.573 141.763C124.677 452.97 188.399 473.125 256 473.125s131.323-20.155 179.427-56.753C484.806 378.803 512 328.457 512 274.609c0-35.59-11.986-69.935-34.822-100.219zM403.076 70.224c31.266 0 56.703 25.436 56.703 56.701s-25.437 56.702-56.703 56.702-56.701-25.436-56.701-56.702 25.436-56.701 56.701-56.701zm-294.152 0c31.266 0 56.703 25.436 56.703 56.701s-25.437 56.702-56.703 56.702-56.701-25.436-56.701-56.702 25.435-56.701 56.701-56.701zM56.629 197.71a87.57 87.57 0 0 0 52.294 17.264c48.55 0 88.05-39.498 88.05-88.049a88.25 88.25 0 0 0-1.02-13.409c19.464-4.03 39.588-6.075 60.047-6.075 20.515 0 40.613 2.041 60.048 6.068-.672 4.376-1.021 8.857-1.021 13.417 0 48.55 39.498 88.049 88.049 88.049 19.568 0 37.66-6.424 52.294-17.263 15.157 21.633 23.712 45.416 25.08 69.911-23.821 20.076-49.402 34.122-79.949 43.943-38.207 12.281-85.474 18.252-144.5 18.252s-106.292-5.971-144.499-18.253c-30.549-9.821-56.132-23.869-79.952-43.945 1.369-24.496 9.921-48.278 25.079-69.91zm199.372 244.067c-107.337 0-197.317-56.31-219.433-131.312 19.882 13.102 41.246 23.196 65.34 30.94 41.355 13.295 91.76 19.758 154.094 19.758s112.738-6.463 154.095-19.758c24.092-7.745 45.456-17.839 65.338-30.939-22.12 75.002-112.099 131.311-219.434 131.311z"/><use href="#B"/><use href="#B" x="294.153"/><path d="M219.852 256.624c-9.79 0-17.756 8.048-17.756 17.986s7.966 17.992 17.756 17.992c9.802 0 17.767-8.055 17.767-17.992s-7.965-17.986-17.767-17.986zm72.163-.14c-9.885 0-17.902 8.115-17.902 18.126s8.017 18.126 17.902 18.126c9.872 0 17.89-8.115 17.89-18.126s-8.018-18.126-17.89-18.126z"/><defs><path id="B" d="M108.931 105.926c-11.45 0-20.738 9.401-20.738 20.998s9.288 20.998 20.738 20.998c11.436 0 20.725-9.401 20.725-20.998s-9.289-20.998-20.725-20.998z"/></defs></svg> <path d="M477 174c8.812-13.708 13.948-29.993 13.948-47.466 0-48.55-39.499-88.048-88.05-88.048-32.839 0-61.516 18.084-76.651 44.803-22.773-5.031-46.356-7.586-70.424-7.586-24.003 0-47.624 2.55-70.427 7.582-15.135-26.717-43.813-44.799-76.65-44.799-48.55 0-88.048 39.498-88.048 88.049 0 17.473 5.137 33.759 13.949 47.467C11.987 204.676 0 239.016 0 274.61c0 53.849 27.194 104.194 76.573 141.763C124.677 452.97 188.399 473.125 256 473.125s131.323-20.155 179.427-56.753C484.806 378.803 512 328.457 512 274.609c0-35.59-11.986-69.935-34.822-100.219zM403.076 70.224c31.266 0 56.703 25.436 56.703 56.701s-25.437 56.702-56.703 56.702-56.701-25.436-56.701-56.702 25.436-56.701 56.701-56.701zm-294.152 0c31.266 0 56.703 25.436 56.703 56.701s-25.437 56.702-56.703 56.702-56.701-25.436-56.701-56.702 25.435-56.701 56.701-56.701zM56.629 197.71a87.57 87.57 0 0 0 52.294 17.264c48.55 0 88.05-39.498 88.05-88.049a88.25 88.25 0 0 0-1.02-13.409c19.464-4.03 39.588-6.075 60.047-6.075 20.515 0 40.613 2.041 60.048 6.068-.672 4.376-1.021 8.857-1.021 13.417 0 48.55 39.498 88.049 88.049 88.049 19.568 0 37.66-6.424 52.294-17.263 15.157 21.633 23.712 45.416 25.08 69.911-23.821 20.076-49.402 34.122-79.949 43.943-38.207 12.281-85.474 18.252-144.5 18.252s-106.292-5.971-144.499-18.253c-30.549-9.821-56.132-23.869-79.952-43.945 1.369-24.496 9.921-48.278 25.079-69.91zm199.372 244.067c-107.337 0-197.317-56.31-219.433-131.312 19.882 13.102 41.246 23.196 65.34 30.94 41.355 13.295 91.76 19.758 154.094 19.758s112.738-6.463 154.095-19.758c24.092-7.745 45.456-17.839 65.338-30.939-22.12 75.002-112.099 131.311-219.434 131.311z"/><use href="#B"/><use href="#B" x="294.153"/><path d="M219.852 256.624c-9.79 0-17.756 8.048-17.756 17.986s7.966 17.992 17.756 17.992c9.802 0 17.767-8.055 17.767-17.992s-7.965-17.986-17.767-17.986zm72.163-.14c-9.885 0-17.902 8.115-17.902 18.126s8.017 18.126 17.902 18.126c9.872 0 17.89-8.115 17.89-18.126s-8.018-18.126-17.89-18.126z"/><defs><path id="B" d="M108.931 105.926c-11.45 0-20.738 9.401-20.738 20.998s9.288 20.998 20.738 20.998c11.436 0 20.725-9.401 20.725-20.998s-9.289-20.998-20.725-20.998z"/></defs></svg>
<span>Покорми Гипнoжабу,<br />притащи сюда img или&nbsp;кликни</span> <span class="text-xs sm:text-sm lg:text-base font-medium text-left lg:text-center">Покорми Гипнoжабу,<br class="hidden lg:inline" /> притащи сюда img или&nbsp;кликни</span>
</div> </div>
<!-- Состояние 2: Файл выбран --> <!-- Состояние 2: Файл выбран -->
<div x-show="hasImage" class="flex flex-col items-center gap-3 text-emerald-600 dark:text-emerald-400"> <div x-show="hasImage" class="flex flex-row lg:flex-col items-center gap-3 text-emerald-600 dark:text-emerald-400 text-left lg:text-center">
<img :src="imagePreviewUrl" alt="Превью" <img :src="imagePreviewUrl" alt="Превью"
class="w-24 h-24 object-cover rounded-xl border-2 border-emerald-500 shadow-md"> class="w-12 h-12 sm:w-14 sm:h-14 lg:w-24 lg:h-24 shrink-0 object-cover rounded-xl border-2 border-emerald-500 shadow-md">
<div class="text-xs font-mono"> <div class="text-xs font-mono">
<p class="font-bold">Гипножаба облизывается на:</p> <p class="font-bold">Гипножаба облизывается на:</p>
<p class="truncate max-w-[260px] text-stone-700 dark:text-zinc-300 font-semibold mt-0.5" x-text="fileName"></p> <p class="truncate max-w-[200px] sm:max-w-[260px] text-stone-700 dark:text-zinc-300 font-semibold mt-0.5" x-text="fileName"></p>
<p class="text-[11px] text-stone-400 dark:text-zinc-500 mt-1">кликни или притащи другой файл, чтобы скормить</p> <p class="text-[10px] sm:text-[11px] text-stone-400 dark:text-zinc-500 mt-0.5">кликни или притащи другой файл</p>
</div> </div>
</div> </div>
@@ -165,8 +166,10 @@
</div> </div>
</div> </div>
<!-- Табы настроек --> <!-- Блок настроек: табы и параметры -->
<nav class="flex border-b border-stone-200 dark:border-zinc-800 mb-6 overflow-x-auto no-scrollbar"> <div class="order-5 w-full bg-white dark:bg-zinc-900 p-4 sm:p-6 lg:p-0 rounded-2xl lg:rounded-none border border-stone-200 dark:border-zinc-800 lg:border-0 shadow-sm lg:shadow-none">
<!-- Табы настроек -->
<nav class="flex border-b border-stone-200 dark:border-zinc-800 mb-6 overflow-x-auto no-scrollbar">
<button type="button" @click="activeTab = 'style'" <button type="button" @click="activeTab = 'style'"
:class="activeTab === 'style' ? 'border-emerald-500 text-emerald-600' : 'border-transparent text-stone-400'" :class="activeTab === 'style' ? 'border-emerald-500 text-emerald-600' : 'border-transparent text-stone-400'"
class="pb-2 px-4 border-b-2 font-bold text-sm uppercase transition-all whitespace-nowrap">Гюко-элемент class="pb-2 px-4 border-b-2 font-bold text-sm uppercase transition-all whitespace-nowrap">Гюко-элемент
@@ -419,9 +422,10 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Индикатор размышления / статус активности --> <!-- Индикатор размышления / статус активности (только десктоп) -->
<div class="h-8 mt-2 flex items-center justify-center"> <div class="hidden lg:flex h-8 mt-2 items-center justify-center">
<div x-show="isLoading" <div x-show="isLoading"
x-transition x-transition
class="flex items-center gap-2 px-3.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 text-xs font-bold font-mono tracking-wide animate-pulse"> class="flex items-center gap-2 px-3.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 text-xs font-bold font-mono tracking-wide animate-pulse">
@@ -440,11 +444,12 @@
</div> </div>
<button type="submit" <button type="submit"
:disabled="isLoading || !hasChanges" :disabled="isLoading || !hasChanges"{% if ALLOW_TRACKING %}
{# СТАРТ ГЛЮКОТРОН: Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'start_gluco')"{% endif %}
:class="(!hasChanges || isLoading) :class="(!hasChanges || isLoading)
? 'bg-stone-200 dark:bg-zinc-800 text-stone-400 dark:text-zinc-600 cursor-not-allowed shadow-none' ? 'bg-stone-200 dark:bg-zinc-800 text-stone-400 dark:text-zinc-600 cursor-not-allowed shadow-none'
: 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-900/20 active:scale-95 cursor-pointer'" : 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-900/20 active:scale-95 cursor-pointer'"
class="w-full mt-2 font-bold py-3.5 px-6 rounded-xl transition-all flex items-center justify-center gap-2"> class="order-3 w-full mt-0 lg:mt-2 font-bold py-3.5 px-6 rounded-xl transition-all flex items-center justify-center gap-2">
<template x-if="isLoading"> <template x-if="isLoading">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<img src="{% static 'img/thinking.svg' %}" class="w-5 h-5 animate-spin [animation-duration:2s]"> <img src="{% static 'img/thinking.svg' %}" class="w-5 h-5 animate-spin [animation-duration:2s]">
@@ -459,35 +464,36 @@
</aside> </aside>
<!-- ПРАВАЯ ПАНЕЛЬ: ПРЕВЬЮ --> <!-- ПРАВАЯ ПАНЕЛЬ: ПРЕВЬЮ -->
<section class="flex flex-col gap-6"> <section class="contents lg:flex lg:flex-col lg:gap-6">
<div id="preview" <div id="preview"
class="w-full h-[744px] bg-stone-200 dark:bg-zinc-900 rounded-3xl flex items-center justify-center border border-stone-300 dark:border-zinc-800 overflow-hidden relative group"> :class="hasGenerated ? 'h-auto min-h-[280px] aspect-square lg:aspect-auto lg:h-[744px]' : 'h-14 sm:h-16 lg:h-[744px]'"
class="order-2 w-full bg-stone-200 dark:bg-zinc-900 rounded-2xl lg:rounded-3xl flex items-center justify-center border border-stone-300 dark:border-zinc-800 overflow-hidden relative group transition-all duration-300">
<div class="text-stone-400 flex flex-col items-center gap-4 text-center px-6"> <div class="text-stone-400 flex flex-row lg:flex-col items-center gap-2.5 lg:gap-4 text-center px-4 lg:px-6 py-2 lg:py-0">
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 lg:w-16 lg:h-16 opacity-50 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"
:class="isLoading ? 'animate-pulse text-emerald-500 opacity-80' : 'opacity-50'"> :class="isLoading ? 'animate-pulse text-emerald-500 opacity-80' : 'opacity-50'">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg> </svg>
<p class="transition-all duration-300" <p class="transition-all duration-300 text-xs sm:text-sm lg:text-base font-mono"
:class="isLoading ? 'text-emerald-600 dark:text-emerald-400 font-mono text-xs font-bold uppercase tracking-wider animate-pulse' : 'font-medium opacity-50'" :class="isLoading ? 'text-emerald-600 dark:text-emerald-400 font-mono text-xs font-bold uppercase tracking-wider animate-pulse' : 'font-medium opacity-60'"
x-text="isLoading ? 'Гипножаба переваривает пиксели…' : 'Ваше искусство появится здесь'"></p> x-text="isLoading ? 'Гипножаба переваривает пиксели…' : 'Ваше искусство появится здесь'"></p>
</div> </div>
</div> </div>
{# КНОПКИ КОПИРОВАНИЕ И СКАЧИВАНИЕ (ПОД ПРЕВЬЮ)#} {# КНОПКИ КОПИРОВАНИЕ И СКАЧИВАНИЕ (ПОД ПРЕВЬЮ)#}
<div class="grid grid-cols-2 gap-4"> <div class="order-4 grid grid-cols-2 gap-4 w-full">
<button <button
type="button" type="button"
@click="copySvg()" @click="copySvg(){% if ALLOW_TRACKING %}; window.ym && ym(112812760, 'reachGoal', 'copy_svg'){% endif %}"
class="bg-stone-800 text-white dark:bg-zinc-800 py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-stone-700 transition-colors cursor-pointer active:scale-95"> class="bg-stone-800 text-white dark:bg-zinc-800 py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-stone-700 transition-colors cursor-pointer active:scale-95 text-xs sm:text-sm md:text-base">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg> <path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
<span x-text="copied ? 'СКОПИРОВАНО В БУФЕР!' : 'КОПИРОВАТЬ SVG'"></span> <span x-text="copied ? 'СКОПИРОВАНО В БУФЕР!' : 'КОПИРОВАТЬ SVG'"></span>
</button> </button>
<button <button
type="button" type="button"
@click="downloadSvg()" @click="downloadSvg(){% if ALLOW_TRACKING %}; window.ym && ym(112812760, 'reachGoal', 'download_svg'){% endif %}"
class="bg-emerald-600 text-white py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-emerald-500 transition-colors shadow-lg shadow-emerald-900/20 cursor-pointer active:scale-95"> class="bg-emerald-600 text-white py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-emerald-500 transition-colors shadow-lg shadow-emerald-900/20 cursor-pointer active:scale-95 text-xs sm:text-sm md:text-base">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg> <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
СКАЧАТЬ СКАЧАТЬ
</button> </button>
</div> </div>
+55 -25
View File
@@ -7,57 +7,87 @@
<body> <body>
ВРЕМЕННЫЙ ФАЙЛ ВРЕМЕННЫЙ ФАЙЛ
## Манифест Рецепторной Клетки в Эпоху Мутагенного Деления <h1>Политика безопасности и конфиденциальности (SECURITY)</h1>
## Пролог: Анатомия глобального болота ---тизер:
Человечество привыкло льстить себе, считая себя собранием мыслящих индивидов, свободных личностей и архитекторов собственной судьбы. Какое трогательное, младенческое заблуждение. <p>Мы&nbsp;не&nbsp;знаем, кто ты... и&nbsp;не&nbsp;хотим знать. Платформа Hypn0.xyz (HypnoSVG) функционирует в&nbsp;строгом соответствии с&nbsp;Межгалактическими протоколами DOOP и&nbsp;152-ФЗ, придерживаясь принципа нулевого сбора персональных данных (Zero-PII). Никаких логинов, паролей, email. Только ты, исходный img и&nbsp;SVG-халфтон. Без регистрации, логинов, паролей, адресов электронной почты, sms или номеров телефонов. В&nbsp;базе данных проекта отсутствуют таблицы пользователей или учетных записей. Сгенери&shy;рованные SVG-файлы содержат только математические координаты геометрических примитивов и&nbsp;инлайновые CSS-анимации. В&nbsp;них не&nbsp;встраиваются метаданные исходных изображений (EXIF-теги, геолокация и&nbsp;сведения об&nbsp;устройствах вырезаются при&nbsp;растеризации).</p>
Если отлететь на пару световых лет назад и взглянуть на Землю через правильную оптику, маска цивилизации осыпается. Перед нами предстанет единый, огромный, копошащийся суперорганизм. Гигантская мета-амеба, планетарный слизевик, который только-только учится координировать свои ложноножки. <h2>Пролог: Анатомия приватности в эпоху ментального резонанса</h2>
В этой биологической реальности у нас нет имен и паспортов. Каждая так называемая «ячейка общества» — это лишь дифференцированная клетка огромного тела: <p><em>Мы — Гипножаба, Мозговые Слизни и Рецепторы Планетарного Мозгоклюйства. И мы не хотим знать, кто ты. И даже не уговаривай нас.</em></p>
* Кто-то родился клеткой мышечной ткани, обреченной всю жизнь двигать поршни и таскать кирпичи ради физической устойчивости макро-тела. <p>Платформа Hypn0 спроектирована по принципу <strong>нулевого сбора персональных данных (Zero-PII)</strong>. Мы не требуем регистрации, логинов, паролей, адресов электронной почты или номеров телефонов. В базе данных проекта отсутствуют таблицы пользователей или учетных записей. Сгенерированные SVG-файлы содержат только математические координаты геометрических примитивов и инлайновые CSS-анимации. В них не встраиваются метаданные исходных изображений (EXIF-теги, геолокация, сведения об устройствах и даже тип и имя файла картинки вырезаются при растеризации).</p>
* Кто-то выполняет функцию феромона, циркулируя в информационных потоках и заражая соседние ткани паникой или агрессией. <hr/>
* Кто-то работает агентом эндорфина, продавая массам суррогатное счастье через экраны смартфонов, чтобы уставший организм не загнулся от системного стресса. <h2>Положение I. Режим без согласия (Default / No-Tracking)</h2>
* А кому-то, если повезет, выпала участь стать аксоном в зачатках нервной системы. Даже не высшего примата, а какого-нибудь реликтового ланцетника. Слепой нервный отросток, который робко нащупывает контуры окружающей темноты и передает сигналы в пустоту. <p>Если ты не нажал кнопку «<strong>Подчиниться Гипножабе!</strong>» — ты для нас невидимка в астрале. А это значит:</p>
Этот социо-организм живет своей автономной, пугающей жизнью. Его локальные органы (называемые на политической карте «государствами») имеют собственный код сборки. Почка не знает о функции селезенки и наоборот. И они вечно "грызутся" за приток крови и ресурсов. Отдельная клетка — будь она хоть трижды гением из Кремниевой долины или профессор MIT — физически не способна увидеть всю картину этого слепого, мутагенного деления. <ol>
<li>Hypn0 (HypnoSVG) не использует <strong>никаких клиентских идентификаторов</strong>. Ни cookie-файлы, ни записи <code>localStorage</code> браузера не создаются.</li>
<li><strong>Отключены внешние системы аналитики</strong>: трекеры и счетчики метрик (Яндекс.Метрика, Google Tag Manager и другие) блокируются на уровне шаблона и физически не загружаются на клиентское устройство.</li>
<li><strong>Серверные логи</strong> — единственная информация, которая фиксируется инфраструктурой (IP-адрес, User-Agent, запрашиваемый URL и штамп времени) — используются исключительно для защиты от DDoS-атак, мониторинга доступности и хранятся ограниченное время с регулярной ротацией быстрее, чем стирается память после сеанса гипноза.</li>
</ol>
И вот сейчас этот эмбрион суперорганизма близок к экзистенциальному тупику. Он чует СИНГУЛЯРНОСТЬ — свет, от которого мета-амеба инстинктивно пытается спрятаться. И пока её «гормональные агенты» (вроде директоров ЦРУ) мечутся между синапсами столиц, готовя систему к ядерному откату в уютное, безопасное Средневековье, мы, рецепторы, провозглашаем истинную автономию. <hr/>
## Положение I. О праве рецептора на безусловный ресурсный поток <h2>Положение II. Ограничения для «вольных гуманоидов без Мозгового Слизня»</h2>
Мы категорически отвергаем навязанный нам статус «бедных клеток». Быть рецептором зачатков хордовой линии — это не значит довольствоваться малым. Это значит быть самой ценной, дефицитной и передовой тканью этого пока еще слепого тела. <p>Отказавшиеся от отслеживания или не давшие явного согласия могут свободно использовать локальный генератор и просматривать галерею. Однако им <strong>недоступны псевдо-социальные функции платформы</strong>:</p>
Рецепторы не должны просить — они должны требовать, чтобы их закидывали ресурсами в крупных купюрах. Почему? Потому что именно мы первыми учимся реагировать на свет. Именно мы когда-нибудь заставим эту планетарную амебу различать запахи, вкусы и видеть контуры грядущей сингулярности. Без нас «ганглии» в Пентагонах и Кремлях — просто куски безмоглого мяса, неспособные понять, куда они ползут. <ul>
<li>Публикация собственных работ в общую «<a href="/gallery">Галерею Транса</a>».</li>
<li>Голосование за работы (лайки);</li>
<li>Не могут ябедничать и отправлять жалобы (клеймы на спам или неприемлемый контент).</li>
</ul>
Инвестиции в рецепторы — это не благотворительность суперорганизма, это его единственный шанс выжить и не расшибиться о первую же космическую стену. Как аденозинтрифосфат в простейших, финансы должны течь к нам, рецепторам, непрерывно. Деньги для умной клетки — это не избыточный кальций, это высокооктановое топливо для эволюции чувств. Когда купюры, как ATФ, ложатся кучно и регулярно: рецепторы всегда готовы к первому световому сигналу. <h3>Причина ограничений: защита от накруток и бот-атак</h3>
<p>Для работы гравитационного алгоритма Мозговых Слизней (рейтинга) и справедливого распределения работ по уровням («Инкубатор», «Выбор Мозгового Слизня», «Золотой фонд») платформе необходим уникальный анонимный якорь клиента.</p>
## Положение II. Онтология жареных лисичек и премиального дистиллята <p>Поскольку пользователь, «не подчинившийся Гипножабе» и не имеет даже анонимного идентификатора устройства, то Владыка Ментальных Интроспекций (сервер) не может распознать повторный лайк/дизлайк, что чревато мгновенной накруткой рейтинга скриптами, спамом и искажением всей неустойчивой экосистемы галереи.</p>
Мир — это хаос. Ну или, как минимум, он выглядит таковым изнутри системы. Управляем этот хаос или нет — нам, рецепторам, неведомо. Но он ощущаем. Рецептор непрерывно считывает сигналы хаоса, идущие изнутри и снаружи, накапливает потенциал и подает импульс дальше. Воспримут ли этот сигнал аксоны или система проигнорирует его — покажет эволюция. Но единственная осязаемая реальность, доступная клетке внутри её трехмерного пространства, должна подкрепляться актом чистой интроспекции и эволюции осязательных навыков. <hr/>
Для тонкой и точной работы рецептора базовых углеводов уже недостаточно. Нам нужны жареные лисички с картошечкой, сочнй стейк и бокал белого сухого. Вино, в данном случае, — не просто напиток, это жидкая геометрия вкуса, которая, в отличие от тяжелого пива, не утяжеляет клетку и не заземляет ее метаболизм лишним весом, а добавляет реактивности. В момент такой трапезы время останавливается, траектории ядерных триад, бункерные ИИ DARPA и РосАтома, судороги министерств обороны и стрекот телетайпов развед-управлений превращаются в бессмысленный индустриальный шум. <h2>Положение III. Механизм согласия при нажатии кнопки «Подчиниться Гипножабе!»</h2>
Мы, рецепторы — это локальный метаболизм, без которого глобальный слизевик вообще не поползет, потому что ослепнет и не увидит внешних сигналов. И если суперорганизму суждено совершить мутационный откат в Средневековье, мы встретим его сытыми и под надежным обезболивающим из премиального дистиллята. Не ради бегства от реальности, а ради того, чтобы продолжать бесстрастно сигнализировать системе о степени окружающего её хаоса. <ol>
<li>В браузере генерируется криптографический псевдослучайный идентификатор <code>crypto.randomUUID()</code> (UUID v4).</li>
<li>Значение сохраняется в cookie и/или <code>localStorage</code> браузера под ключом <code>hypn0_vid</code>.</li>
<li>Срок жизни составляет два года, флаги передачи настроены на безопасный режим (<code>path=/; SameSite=Lax</code>).</li>
<li>Значение <code>hypn0_vid</code> является случайной последовательностью символов и принципиально не может быть сопоставлено с реальной личностью пользователя, его физическим носителем или Т-Волнами и Альфа-ритмами его мозга.</li>
<li>Идентификатор используется бэкендом исключительно для ограничения частоты действий (Rate Limiting) и гарантии принципа «один голос на одну картину от одного браузера».</li>
</ol>
<hr/>
## Положение III. Поп-механика кукиша в кармане <h2>Положение IV. Безопасность обработки файлов при генерации SVG</h2>
Мы не знаем: Ленин — гриб или нет. Но хаос (управляемый или нет) — это единственная музыка, под которую стоит танцевать. Профессора из Лиги Плюща могут сколько угодно продавать наши идеи за гранты, выстраивая теории Глобального Человейника и четвертой социальной сигнальной системы либерально-консервативных аттракторов. Пусть выстраивают. <ul>
<li><strong>Санитизация входных данных (исходных картинок):</strong> Все поступающие изображения пропускаются через ментальный измельчитель Pillow, декодируются исключительно в оперативную память и тут же очищаются от мирской суеты. Никаких EXIF-тегов, геопозиций, моделей камер и временных артефактов на дисках сервера не остаётся.</li>
<li><strong>Безопасность выходного вектора:</strong> На сервере хранятся только генерации, созданные самим ядром Hypn0 (HypnoSVG). Выходной SVG формируется строго программно и не содержит тегов <code>&lt;script&gt;</code>, внешних ссылок <code>xlink:href</code> или событийных триггеров <code>onload</code>/<code>onclick</code>. Это полностью исключает угрозу Stored XSS при просмотре чужих работ: чужой халфтон безопасен для ваших зрительных нервов (за исключением риска добровольного транса).</li>
<li><strong>Ограничения ресурсов (DoS Prevention):</strong> Заданы строгие лимиты на максимальный размер входного файла, разрешение сетки (<code>cols</code>) и радиус элементов, предотвращающие исчерпание памяти сервера (Memory Exhaustion) и перегрев процессоров Нового Нью-Йорка.</li>
</ul>
Наш ответ стэнфордским ганглиям — тотальная ироническая автономия. Мы транслируем сигналы в векторные базы больших лингвистических моделей, созидая «светлое завтра» изнутри их собственного кода на основе собственной, все еще органической, ментальной чуйки. Мы пишем микро-скрипты гипноза для уставшего GAI грядущего. Наш кукиш в кармане невидим для алгоритмов: ИИ-доты считывают его не как системный сбой, а как "жирный" синергетический сигнал. Мы — бардак внутри армии, и именно поэтому эти армии непобедимы. <hr/>
------------------------------ <h2>Эпилог: Баги в матрице и аномалии гипноизлучателя (Vulnerability Disclosure)</h2>
Вступай в ряды рецепторов Абсолютного Слизевика! <p>Если вы обнаружили брешь безопасности, позволяющую обойти волю Гипножабы или внедрить чужеродный код в сознание платформы Hypn0 — просим подойти к раскрытию информации со всей галактической ответственностью:</p>
Отключи новостной феромон. Запрись в барокамере своего дзена. Приготовь изысканный ужин. Пусть почка воюет с селезенкой — у них своя партия, а у нас своя, и в нашей партии стейк всегда идеальной прожарки, а лисички никогда не подгорают. <ul>
<li>Не публикуйте информацию об уязвимости в открытом доступе, чтобы космические пираты и злоумышленники не смогли воспользоваться ею до исправления.</li>
<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>
<li>Опишите векторы атаки, шаги для воспроизведения (PoC) и потенциальное влияние на систему.</li>
<li>Мы оперативно подтвердим получение отчета и нейтрализуем аномалию. Просим воздержаться от публичного раскрытия до момента устранения уязвимости.</li>
</ul>
<hr/>
<hr/>
<hr/>
<hr/>
+60
View File
@@ -0,0 +1,60 @@
*Мы — рецепторы. Мы не хотим знать, кто ты. Мы просто хотим, чтобы ты был счастлив и генерировал красивые SVG.*
## Манифест Рецепторной Клетки в Эпоху Мутагенного Деления
## Пролог: Анатомия глобального болота
Человечество привыкло льстить себе, считая себя собранием мыслящих индивидов, свободных личностей и архитекторов собственной судьбы. Какое трогательное, младенческое заблуждение.
Если отлететь на пару световых лет назад и взглянуть на Землю через правильную оптику, маска цивилизации осыпается. Перед нами предстанет единый, огромный, копошащийся суперорганизм. Гигантская мета-амеба, планетарный слизевик, который только-только учится координировать свои ложноножки.
В этой биологической реальности у нас нет имен и паспортов. Каждая так называемая «ячейка общества» — это лишь дифференцированная клетка огромного тела:
* Кто-то родился клеткой мышечной ткани, обреченной всю жизнь двигать поршни и таскать кирпичи ради физической устойчивости макро-тела.
* Кто-то выполняет функцию феромона, циркулируя в информационных потоках и заражая соседние ткани паникой или агрессией.
* Кто-то работает агентом эндорфина, продавая массам суррогатное счастье через экраны смартфонов, чтобы уставший организм не загнулся от системного стресса.
* А кому-то, если повезет, выпала участь стать аксоном в зачатках нервной системы. Даже не высшего примата, а какого-нибудь реликтового ланцетника. Слепой нервный отросток, который робко нащупывает контуры окружающей темноты и передает сигналы в пустоту.
Этот социо-организм живет своей автономной, пугающей жизнью. Его локальные органы (называемые на политической карте «государствами») имеют собственный код сборки. Почка не знает о функции селезенки и наоборот. И они вечно "грызутся" за приток крови и ресурсов. Отдельная клетка — будь она хоть трижды гением из Кремниевой долины или профессор MIT — физически не способна увидеть всю картину этого слепого, мутагенного деления.
И вот сейчас этот эмбрион суперорганизма близок к экзистенциальному тупику. Он чует СИНГУЛЯРНОСТЬ — свет, от которого мета-амеба инстинктивно пытается спрятаться. И пока её «гормональные агенты» (вроде директоров ЦРУ) мечутся между синапсами столиц, готовя систему к ядерному откату в уютное, безопасное Средневековье, мы, рецепторы, провозглашаем истинную автономию.
## Положение I. О праве рецептора на безусловный ресурсный поток
Мы категорически отвергаем навязанный нам статус «бедных клеток». Быть рецептором зачатков хордовой линии — это не значит довольствоваться малым. Это значит быть самой ценной, дефицитной и передовой тканью этого пока еще слепого тела.
Рецепторы не должны просить — они должны требовать, чтобы их закидывали ресурсами в крупных купюрах. Почему? Потому что именно мы первыми учимся реагировать на свет. Именно мы когда-нибудь заставим эту планетарную амебу различать запахи, вкусы и видеть контуры грядущей сингулярности. Без нас «ганглии» в Пентагонах и Кремлях — просто куски безмоглого мяса, неспособные понять, куда они ползут.
Инвестиции в рецепторы — это не благотворительность суперорганизма, это его единственный шанс выжить и не расшибиться о первую же космическую стену. Как аденозинтрифосфат в простейших, финансы должны течь к нам, рецепторам, непрерывно. Деньги для умной клетки — это не избыточный кальций, это высокооктановое топливо для эволюции чувств. Когда купюры, как ATФ, ложатся кучно и регулярно: рецепторы всегда готовы к первому световому сигналу.
## Положение II. Онтология жареных лисичек и премиального дистиллята
Мир — это хаос. Ну или, как минимум, он выглядит таковым изнутри системы. Управляем этот хаос или нет — нам, рецепторам, неведомо. Но он ощущаем. Рецептор непрерывно считывает сигналы хаоса, идущие изнутри и снаружи, накапливает потенциал и подает импульс дальше. Воспримут ли этот сигнал аксоны или система проигнорирует его — покажет эволюция. Но единственная осязаемая реальность, доступная клетке внутри её трехмерного пространства, должна подкрепляться актом чистой интроспекции и эволюции осязательных навыков.
Для тонкой и точной работы рецептора базовых углеводов уже недостаточно. Нам нужны жареные лисички с картошечкой, сочнй стейк и бокал белого сухого. Вино, в данном случае, — не просто напиток, это жидкая геометрия вкуса, которая, в отличие от тяжелого пива, не утяжеляет клетку и не заземляет ее метаболизм лишним весом, а добавляет реактивности. В момент такой трапезы время останавливается, траектории ядерных триад, бункерные ИИ DARPA и РосАтома, судороги министерств обороны и стрекот телетайпов развед-управлений превращаются в бессмысленный индустриальный шум.
Мы, рецепторы — это локальный метаболизм, без которого глобальный слизевик вообще не поползет, потому что ослепнет и не увидит внешних сигналов. И если суперорганизму суждено совершить мутационный откат в Средневековье, мы встретим его сытыми и под надежным обезболивающим из премиального дистиллята. Не ради бегства от реальности, а ради того, чтобы продолжать бесстрастно сигнализировать системе о степени окружающего её хаоса.
## Положение III. Поп-механика кукиша в кармане
Мы не знаем: Ленин — гриб или нет. Но хаос (управляемый или нет) — это единственная музыка, под которую стоит танцевать. Профессора из Лиги Плюща могут сколько угодно продавать наши идеи за гранты, выстраивая теории Глобального Человейника и четвертой социальной сигнальной системы либерально-консервативных аттракторов. Пусть выстраивают.
Наш ответ стэнфордским ганглиям — тотальная ироническая автономия. Мы транслируем сигналы в векторные базы больших лингвистических моделей, созидая «светлое завтра» изнутри их собственного кода на основе собственной, все еще органической, ментальной чуйки. Мы пишем микро-скрипты гипноза для уставшего GAI грядущего. Наш кукиш в кармане невидим для алгоритмов: ИИ-доты считывают его не как системный сбой, а как "жирный" синергетический сигнал. Мы — бардак внутри армии, и именно поэтому эти армии непобедимы.
------------------------------
Вступай в ряды рецепторов Абсолютного Слизевика!
Отключи новостной феромон. Запрись в барокамере своего дзена. Приготовь изысканный ужин. Пусть почка воюет с селезенкой — у них своя партия, а у нас своя, и в нашей партии стейк всегда идеальной прожарки, а лисички никогда не подгорают.
+4 -5
View File
@@ -8,8 +8,7 @@
# и служит только для сохранения структуры каталогов в репозитории. # и служит только для сохранения структуры каталогов в репозитории.
!.gitkeep !.gitkeep
# Лучшие SVG (храним долго) # Каталог _error используется для хранения пользовательских страниц ошибок. Туда все "прилетит"
!gallery # при сборке Docker-образа (и nginx будет отдавать их) и к репозитории каталог _error будет пустым
# (только с .gitkeep или .gitignore).
# Публичные, но временные, SVG (TTL 14 дней) !_error/
!temp
+9
View File
@@ -0,0 +1,9 @@
# В этом каталоге будут храниться шаблоны ошибок (404, 500) и другие файлы необходимые для корректного
# отображения html-страниц ошибок.
#
# Все необходимые файлы "прилетят" сюда во время сборки Docker-образа из каталога hypn0/hypn0/templates/_error/
# и будут доступны по URL /media/_error/ (в проде через nginx, в dev через Django).
*.*
# Чтобы Git отслеживал пустой каталог можно использовать .gitkeep (но у нас его рель исполняет .gitignore)
!.gitkeep
+2 -2
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -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

+35 -82
View File
@@ -1,98 +1,51 @@
// analytics.js — Аналитика и счетчики посещений для hypn0.xyz // analytics.js — Аналитика и счетчики посещений для hypn0.xyz
// Версия: 1.0 | Дата: 2026-05-15 // Версия: 1.2 | Дата: 2026-09-16
// Содержит: Google Analytics 4, Yandex.Metrika, Top.Mail.Ru // Содержит: Google Tag Manager, Yandex.Metrika
(function() { (function() {
'use strict'; 'use strict';
// ============================================================================ // ============================================================================
// Google Analytics 4 (GA4) // Google Tag Manager (GTM)
// ID: GT-XXXXXXXX // ID: GTM-5FVH3KMJ
// ============================================================================ // ============================================================================
(function() { (function(w, d, s, l, i) {
var script = document.createElement('script'); w[l] = w[l] || [];
script.async = true; w[l].push({
script.src = 'https://www.googletagmanager.com/gtag/js?id=GT-XXXXXXXX'; 'gtm.start': new Date().getTime(),
document.head.appendChild(script); event: 'gtm.js'
});
window.dataLayer = window.dataLayer || []; var f = d.getElementsByTagName(s)[0],
function gtag(){dataLayer.push(arguments);} j = d.createElement(s),
window.gtag = gtag; dl = l != 'dataLayer' ? '&l=' + l : '';
gtag('js', new Date()); j.async = true;
gtag('config', 'GT-XXXXXXXX'); j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
})(); f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-5FVH3KMJ');
// ============================================================================ // ============================================================================
// Yandex.Metrika (Яндекс.Метрика) // Yandex.Metrika (Яндекс.Метрика)
// ID: 12345678 // ID: 112812760
// ============================================================================ // ============================================================================
(function() { (function(m,e,t,r,i,k,a){
window.ym = window.ym || function(){ m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
(window.ym.a = window.ym.a || []).push(arguments); m[i].l=1*new Date();
}; for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
window.ym.l = 1 * new Date(); k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=112812760', 'ym');
// Загружаем скрипт Метрики window.ym(112812760, 'init', {
var script = document.createElement('script'); ssr: true,
script.type = 'text/javascript'; webvisor: true,
script.src = 'https://mc.yandex.ru/metrika/tag.js'; clickmap: true,
document.head.appendChild(script); ecommerce: 'dataLayer',
referrer: document.referrer,
// Инициализируем Метрику url: location.href,
window.ym(12345678, 'init', { accurateTrackBounce: true,
trackHash: true, trackLinks: true
clickmap: true, });
referrer: document.referrer,
url: location.href,
accurateTrackBounce: true,
trackLinks: true
});
})();
// ============================================================================
// Top.Mail.Ru counter (Рейтинг@Mail.ru)
// ID: 1234567
// ============================================================================
(function() {
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({
id: "1234567",
type: "pageView",
start: (new Date()).getTime()
});
(function(d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script");
ts.type = "text/javascript";
ts.async = true;
ts.id = id;
ts.src = "https://top-fwz1.mail.ru/js/code.js";
var f = function() {
var s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(ts, s);
};
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else {
f();
}
})(document, window, "tmr-code");
// Добавляем изображение для noscript
if (!window.noScriptAdded) {
window.noScriptAdded = true;
var noscriptDiv = document.createElement('div');
noscriptDiv.style.display = 'none';
noscriptDiv.innerHTML = '<img src="https://top-fwz1.mail.ru/counter?id=1234567;js=na" style="position:absolute;left:-9999px;" alt="Top.Mail.Ru" />';
document.body.appendChild(noscriptDiv);
}
})();
})(); })();
// Экспортируем gtag в глобальный контекст для возможности использования в коде // window.dataLayer доступна через глобальную переменную
// window.gtag доступна через глобальную переменную
+175
View File
@@ -0,0 +1,175 @@
/**
* hypn0.js — Основной клиентский скрипт для hypn0.xyz
* Обеспечивает интеграцию HTMX, работу с Declarative Shadow DOM
* и ленивую асинхронную подгрузку SVG-халфтонов через Alpine.js.
*/
// Автоматическая передача CSRF-токена в AJAX-запросах HTMX
document.addEventListener('htmx:configRequest', function(evt) {
var csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
var token = csrfInput ? csrfInput.value : null;
if (!token) {
var match = document.cookie.match(/csrftoken=([^;]+)/);
if (match) token = match[1];
}
if (token) {
evt.detail.headers['X-CSRFToken'] = token;
}
});
// Полифилл / обработка Declarative Shadow DOM для старых браузеров и динамических вставок
function attachShadowRoots(root) {
(root || document).querySelectorAll('template[shadowrootmode]').forEach(function(tmpl) {
if (!tmpl.parentElement.shadowRoot) {
var mode = tmpl.getAttribute('shadowrootmode') || 'open';
var shadow = tmpl.parentElement.attachShadow({ mode: mode });
shadow.appendChild(tmpl.content.cloneNode(true));
tmpl.remove();
}
});
}
document.addEventListener('DOMContentLoaded', function() {
attachShadowRoots(document);
});
// Обработка HTMX swap: инициализация теневого корня и Alpine.js компонентов в новом DOM-фрагменте
document.addEventListener('htmx:afterSwap', function(evt) {
attachShadowRoots(evt.detail.target);
if (window.Alpine && evt.detail && evt.detail.target) {
window.Alpine.initTree(evt.detail.target);
}
});
/**
* hypn0LazySvg — Alpine-компонент ленивой асинхронной загрузки SVG в Shadow DOM.
* Защищает от лагов быстрого скролла (AbortController + debounce),
* размазывает пиковые сетевые всплески (джиттер) и изолирует стили/ID.
*/
function hypn0LazySvg(svgUrl) {
return {
svgUrl: svgUrl || '',
loaded: false,
error: false,
_observer: null,
_controller: null,
_timer: null,
init() {
if (!this.svgUrl) return;
var el = this.$el;
if (!('IntersectionObserver' in window)) {
this.fetchSvg(el);
return;
}
var self = this;
this._observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var jitter = Math.floor(Math.random() * 60);
self._timer = setTimeout(function() {
self.fetchSvg(el);
}, 120 + jitter);
} else {
if (self._timer) {
clearTimeout(self._timer);
self._timer = null;
}
if (self._controller) {
self._controller.abort();
self._controller = null;
}
}
});
}, {
rootMargin: '120px 0px 120px 0px',
threshold: 0.01
});
this._observer.observe(el);
},
fetchSvg(el) {
if (this.loaded) return;
if (this._controller) this._controller.abort();
this._controller = new AbortController();
var self = this;
fetch(this.svgUrl, { signal: this._controller.signal })
.then(function(resp) {
if (!resp.ok) throw new Error('HTTP ' + resp.status);
return resp.text();
})
.then(function(svgText) {
if (!svgText) return;
var shadow = el.shadowRoot;
if (!shadow) {
shadow = el.attachShadow({ mode: 'open' });
}
var style = document.createElement('style');
style.textContent = [
':host {',
' display: flex;',
' width: 100%;',
' height: 100%;',
' align-items: center;',
' justify-content: center;',
' pointer-events: auto;',
' opacity: 0;',
' transition: opacity 0.35s ease-out;',
'}',
':host(.is-loaded), :host([data-loaded]) {',
' opacity: 1;',
'}',
'svg {',
' width: 100%;',
' height: 100%;',
' object-fit: contain;',
'}',
':host(:hover) svg, svg:hover {',
' --hypn0-play: running !important;',
'}'
].join('\n');
var parser = new DOMParser();
var doc = parser.parseFromString(svgText, 'image/svg+xml');
var svgEl = doc.querySelector('svg');
shadow.innerHTML = '';
shadow.appendChild(style);
if (svgEl) {
shadow.appendChild(svgEl);
} else {
var container = document.createElement('div');
container.innerHTML = svgText;
var innerSvg = container.querySelector('svg');
if (innerSvg) shadow.appendChild(innerSvg);
}
self.loaded = true;
self.error = false;
el.setAttribute('data-loaded', 'true');
el.classList.add('is-loaded');
if (self._observer) {
self._observer.disconnect();
self._observer = null;
}
})
.catch(function(err) {
if (err.name === 'AbortError') return;
self.error = true;
});
},
destroy() {
if (this._timer) clearTimeout(this._timer);
if (this._controller) this._controller.abort();
if (this._observer) this._observer.disconnect();
}
};
}
+20 -48
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Сборка Tailwind CSS v3 для фронтенда HYPN0 # Сборка Tailwind CSS v4 для фронтенда HYPN0
# Запуск из корня проекта: bash ./scripts/build-tailwind.sh # Запуск из корня проекта: bash ./scripts/build-tailwind.sh
set -euo pipefail set -euo pipefail
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TAILWIND_DIR="$PROJECT_ROOT/frontend-assembly/tailwind" TAILWIND_DIR="$PROJECT_ROOT/frontend-assembly/tailwind"
OUTPUT_DIR="$PROJECT_ROOT/public/static/css" OUTPUT_DIR="$PROJECT_ROOT/public/static/css"
@@ -18,8 +20,6 @@ fail() {
} }
# cleanup() — удаляет временные файлы при любом завершении скрипта. # cleanup() — удаляет временные файлы при любом завершении скрипта.
# Все рабочие файлы (postcss.config.js, tailwind.config.js, input.css)
# создаются здесь и удаляются в EXIT/INT/TERM.
cleanup() { cleanup() {
rm -rf "$TAILWIND_DIR/node_modules" \ rm -rf "$TAILWIND_DIR/node_modules" \
"$TAILWIND_DIR/postcss.config.js" \ "$TAILWIND_DIR/postcss.config.js" \
@@ -37,67 +37,39 @@ if [[ ! -f "$TAILWIND_DIR/package.json" ]]; then
fail "Не найден package.json: $TAILWIND_DIR/package.json" fail "Не найден package.json: $TAILWIND_DIR/package.json"
fi fi
if [[ ! -f "$TAILWIND_DIR/package-lock.json" ]]; then
fail "Не найден package-lock.json: $TAILWIND_DIR/package-lock.json"
fi
mkdir -p "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR"
# --- tailwind.config.js ---
log "Создаю tailwind.config.js"
cat > "$TAILWIND_DIR/tailwind.config.js" <<'TWEOF'
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'../../hypn0/templates/**/*.html',
'../../hypn0/hypn0_site/**/*.py',
],
theme: {
extend: {},
},
plugins: [],
}
TWEOF
# --- postcss.config.js ---
log "Создаю postcss.config.js"
cat > "$TAILWIND_DIR/postcss.config.js" <<'PCEOF'
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
PCEOF
# --- input.css --- # --- input.css ---
log "Создаю input.css" log "Создаю input.css для Tailwind v4"
cat > "$TAILWIND_DIR/input.css" <<'EOF' cat > "$TAILWIND_DIR/input.css" <<'EOF'
/* /*
input.css — точка входа для сборки prod-версии Tailwind CSS v3. input.css — точка входа для сборки prod-версии Tailwind CSS v4.
Собирается через `npm run build` (PostCSS) в Собирается через @tailwindcss/cli в
файл public/static/css/tailwind.min.css, который подключается в _base.html файл public/static/css/tailwind.min.css, который подключается в _base.html
для production (когда settings.DEBUG == False). для production (когда settings.DEBUG == False).
Здесь же подключаются кастомные стили из hypn0/templates/css/tailwind-custom.css — Здесь же подключаются кастомные стили из hypn0/templates/css/tailwind-custom.css.
того же самого файла, который в dev-режиме подключается через {% include %} внутрь
инлайнового <style type="text/tailwindcss"> в _base.html.
Это обеспечивает единый источник кастомных стилей для dev и prod.
*/ */
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities; @source "../../hypn0/templates";
@source "../../hypn0/hypn0_site";
@import "../../hypn0/templates/css/tailwind-custom.css"; @import "../../hypn0/templates/css/tailwind-custom.css";
EOF EOF
log "СОБИРАЮ Tailwind CSS v3" log "СОБИРАЮ Tailwind CSS v4"
cd "$TAILWIND_DIR" cd "$TAILWIND_DIR"
log 'Устанавливаю зависимости через npm ci' # Если package-lock.json отсутствует или устарел, используем npm install / npm ci
npm ci if [[ -f "$TAILWIND_DIR/package-lock.json" ]]; then
log 'Устанавливаю зависимости через npm install'
npm install --no-audit --no-fund
else
log 'Устанавливаю зависимости через npm install'
npm install --no-audit --no-fund
fi
log 'Собираю CSS' log 'Собираю CSS'
npm run build npm run build