Author SHA1 Message Date
erjemin e7f16e1a42 mod: update Tailwind CSS configuration to latest version (v4.3.3) for improved styles and compatibility
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 31s
2026-09-28 00:24:58 +03:00
erjemin dc1897cc62 mod: update Russian to English translations in templates and forms for improved accessibility
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 31s
2026-09-27 21:12:57 +03:00
erjemin e7f348d95e add: management command to rename item titles to English with options for preview, batch updates, and custom seed configurations
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 37s
2026-09-27 19:29:16 +03:00
10 changed files with 585 additions and 185 deletions
+14 -14
View File
@@ -6,25 +6,25 @@ class HalftoneGenerateForm(forms.Form):
Форма валидации входных параметров для генерации гипнотического SVG. Форма валидации входных параметров для генерации гипнотического SVG.
""" """
SHAPE_CHOICES = [ SHAPE_CHOICES = [
("circle", "Круг"), ("circle", "Circle"),
("ring", "Кольцо"), ("ring", "Ring"),
("triangle", "Треугольник"), ("triangle", "Triangle"),
("square", "Квадрат"), ("square", "Square"),
("diamond", "Ромб"), ("diamond", "Diamond"),
("hexagon", "Шестиугольник"), ("hexagon", "Hexagon"),
("star", "Звезда"), ("star", "Star"),
("cross", "Крестик"), ("cross", "Cross"),
("line", "Линия"), ("line", "Line"),
("snowflake", "Снежинка"), ("snowflake", "Snowflake"),
("binary", "Матрица (0/1)"), ("binary", "Binary (0/1)"),
("heart", "Сердечко"), ("heart", "Heart"),
] ]
image = forms.ImageField( image = forms.ImageField(
required=True, required=True,
error_messages={ error_messages={
"required": "Покорми Гипножабу изображением!", "required": "Feed the Hypnotoad with an image!",
"invalid_image": "Гипножаба не может переварить этот файл. Нужна картинка (PNG, JPG, WEBP).", "invalid_image": "Hypnotoad cannot digest this file. Image required (PNG, JPG, WEBP).",
}, },
) )
shape = forms.ChoiceField( shape = forms.ChoiceField(
@@ -0,0 +1,164 @@
import re
from typing import Optional
from django.core.management.base import BaseCommand
from django.db import transaction
from hypn0_site.models import TbHypn0Item
from hypn0_site.services.naming import generate_hypno_title
class Command(BaseCommand):
"""
Массовое переименование названий генераций (TbHypn0Item.s_title) на английский язык.
АРХИТЕКТУРНОЕ НАЗНАЧЕНИЕ:
=========================
1. Локализация каталога и прод-базы:
Переводит существующие русскоязычные названия картин в базе данных
на сочные англоязычные гипнотические тайтлы в стиле Гипножабы.
2. Детерминированность vs Случайность:
По умолчанию использует детерминированный сид на базе ID генерации (item.id + seed_offset),
что обеспечивает стабильность результатов при повторных запусках (идемпотентность).
Флаг --random позволяет генерировать полностью случайные тайтлы.
3. Безопасность и оптимизация:
- Пакетное обновление через bulk_update порциями (--batch-size).
- Режим предварительного просмотра (--dry-run).
- Фильтрация по умолчанию только кириллических / пустых названий (или всех с флагом --all).
"""
help = "Массовое переименование названий генераций на английский язык"
def add_arguments(self, parser):
parser.add_argument(
"--dry-run",
action="store_true",
help="Тестовый прогон: показать изменения без сохранения в базу данных",
)
parser.add_argument(
"--all",
action="store_true",
help="Переименовать абсолютно все генерации (по умолчанию только с кириллицей или пустые)",
)
parser.add_argument(
"--only-cyrillic",
action="store_true",
help="Переименовывать только записи с кириллическими символами в названии (включено по умолчанию)",
)
parser.add_argument(
"--batch-size",
type=int,
default=500,
help="Размер пакета для bulk_update (по умолчанию: 500)",
)
parser.add_argument(
"--limit",
type=int,
default=None,
help="Ограничить количество обрабатываемых записей",
)
parser.add_argument(
"--seed-offset",
type=int,
default=0,
help="Смещение для детерминированного генератора сидов (по умолчанию: 0)",
)
parser.add_argument(
"--random",
action="store_true",
help="Использовать недетерминированные случайные сиды вместо ID записи",
)
def handle(self, *args, **options):
dry_run: bool = options["dry_run"]
rename_all: bool = options["all"]
batch_size: int = options["batch_size"]
limit: Optional[int] = options["limit"]
seed_offset: int = options["seed_offset"]
use_random: bool = options["random"]
self.stdout.write(
self.style.NOTICE(
f"Старт процесса переименования генераций "
f"(dry_run={dry_run}, rename_all={rename_all}, batch_size={batch_size}, random={use_random})..."
)
)
cyrillic_pattern = re.compile(r"[а-яА-ЯёЁ]")
qs = TbHypn0Item.objects.all().only("id", "s_title").order_by("id")
total_in_db = qs.count()
if total_in_db == 0:
self.stdout.write(self.style.WARNING("В базе данных нет генераций для обработки."))
return
to_update = []
renamed_count = 0
preview_samples = []
for item in qs.iterator(chunk_size=batch_size):
old_title = item.s_title or ""
# Проверяем критерий необходимости переименования
needs_rename = False
if rename_all:
needs_rename = True
else:
# По умолчанию: кириллица или пустой заголовок
if not old_title.strip() or cyrillic_pattern.search(old_title):
needs_rename = True
if not needs_rename:
continue
# Генерация нового англоязычного тайтла
seed = None if use_random else (item.id + seed_offset)
new_title = generate_hypno_title(seed=seed)
if len(preview_samples) < 15:
preview_samples.append((item.id, old_title, new_title))
item.s_title = new_title
to_update.append(item)
renamed_count += 1
if limit is not None and renamed_count >= limit:
break
# Пакетное сохранение порциями
if not dry_run and len(to_update) >= batch_size:
with transaction.atomic():
TbHypn0Item.objects.bulk_update(to_update, fields=["s_title"])
to_update.clear()
# Сохранение остатка
if not dry_run and to_update:
with transaction.atomic():
TbHypn0Item.objects.bulk_update(to_update, fields=["s_title"])
to_update.clear()
# Вывод примеров переименования
if preview_samples:
self.stdout.write("\nПримеры переименований (первые до 15 записей):")
self.stdout.write("-" * 70)
for item_id, old_t, new_t in preview_samples:
self.stdout.write(f" [ID {item_id:5d}] «{old_t}» -> «{new_t}»")
self.stdout.write("-" * 70)
self.stdout.write(f"\nВсего записей в базе данных: {total_in_db}")
self.stdout.write(f"Отобрано для переименования: {renamed_count}")
if dry_run:
self.stdout.write(
self.style.WARNING(
f"\n[DRY RUN] Проверено {renamed_count} записей. Изменения НЕ сохранены в БД. "
"Запустите без флага --dry-run для применения."
)
)
else:
self.stdout.write(
self.style.SUCCESS(f"\nУспешно переименовано {renamed_count} генераций в базе данных!")
)
+7 -7
View File
@@ -596,13 +596,13 @@ class TbBlogPost(models.Model):
def blog_nav_title(self) -> str: def blog_nav_title(self) -> str:
"""Детерминированный выбор названия раздела блога для навигации на основе ID статьи.""" """Детерминированный выбор названия раздела блога для навигации на основе ID статьи."""
nav_titles = ( nav_titles = (
"Блог Мозгоклюя", "Brain Slug Blog",
"Записки Гипножабы", "Hypnotoad Field Notes",
"Дневник Мозговых Перцепторов", "Neural Perceptor Diary",
"Хроники психо-резонанса", "Psycho-Resonance Chronicles",
"Мемуары астрального архива", "Astral Archive Memoirs",
"Манифесты волновой матрицы", "Wave Matrix Manifestos",
"Бортовой журнал транс-погружения", "Deep Trance Flight Log",
) )
if not self.id: if not self.id:
return nav_titles[0] return nav_titles[0]
+1 -1
View File
@@ -47,7 +47,7 @@ HYPNO_GENITIVES = [
HYPNO_POSTFIXES = [ HYPNO_POSTFIXES = [
"#42", "v3.14", "(Ultra)", "(Phase&nbsp;7)", "(Alpha)", "(Omega)", "(Protocol&nbsp;09)", "(Revision&nbsp;14)", "#42", "v3.14", "(Ultra)", "(Phase&nbsp;7)", "(Alpha)", "(Omega)", "(Protocol&nbsp;09)", "(Revision&nbsp;14)",
"(Extract)", "(Layer&nbsp;9)", "(Ward&nbsp;106)", "(Hangar&nbsp;18)", "(Unfinished Opus)", "(Secret Project)", "(Extract)", "(Layer&nbsp;9)", "(Ward&nbsp;106)", "(Hangar&nbsp;18)", "(Unfinished Opus)", "(Secret Project)",
"[Classified]", "(DOOP-66)", "(Sub-Level&nbsp;4)", "(MK-Ultra)", "С90" "[Classified]", "(DOOP-66)", "(Sub-Level&nbsp;4)", "(MK-Ultra)", "C90"
] ]
+65 -65
View File
@@ -232,7 +232,7 @@ class HalftoneViewTests(BaseMediaTestCase):
self.assertContains(response, 'X-CSRFToken') self.assertContains(response, 'X-CSRFToken')
self.assertContains(response, "handleFileChange") self.assertContains(response, "handleFileChange")
self.assertContains(response, "imagePreviewUrl") self.assertContains(response, "imagePreviewUrl")
self.assertContains(response, "Гипножаба облизывается на:") self.assertContains(response, "Hypnotoad salivates over:")
# Проверяем, что csrf_token не пустой в hx-headers # Проверяем, что csrf_token не пустой в hx-headers
content = response.content.decode("utf-8") content = response.content.decode("utf-8")
self.assertNotIn('hx-headers=\'{"X-CSRFToken": ""}\'', content) self.assertNotIn('hx-headers=\'{"X-CSRFToken": ""}\'', content)
@@ -291,7 +291,7 @@ class HalftoneViewTests(BaseMediaTestCase):
def test_generate_post_no_image(self): def test_generate_post_no_image(self):
response = self.client.post(reverse("hypn0_site:generate"), data={"shape": "circle"}) response = self.client.post(reverse("hypn0_site:generate"), data={"shape": "circle"})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Покорми Гипножабу") self.assertContains(response, "Feed the Hypnotoad")
def test_generate_post_success(self): def test_generate_post_success(self):
buf = io.BytesIO() buf = io.BytesIO()
@@ -366,8 +366,8 @@ class PublishViewTests(BaseMediaTestCase):
}, },
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Мозговые слизняки протестуют!") self.assertContains(response, "Brain Slugs are&nbsp;rioting!")
self.assertContains(response, "Подчиниться и опубликовать") self.assertContains(response, "Submit and&nbsp;Publish")
self.assertEqual(TbHypn0Item.objects.count(), 0) self.assertEqual(TbHypn0Item.objects.count(), 0)
def test_publish_with_cookie_success(self): def test_publish_with_cookie_success(self):
@@ -389,8 +389,8 @@ class PublishViewTests(BaseMediaTestCase):
}, },
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Уже в галерее транса") self.assertContains(response, "In&nbsp;the&nbsp;Trance Gallery")
self.assertContains(response, "идет прорастание психо-паутины...") self.assertContains(response, "psychic neural web expanding&hellip;")
# Проверяем запись в БД # Проверяем запись в БД
self.assertEqual(TbHypn0Item.objects.count(), 1) self.assertEqual(TbHypn0Item.objects.count(), 1)
@@ -419,7 +419,7 @@ class PublishViewTests(BaseMediaTestCase):
data={"svg_content": ""}, data={"svg_content": ""},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Нет данных SVG для публикации") self.assertContains(response, "No SVG data for publication")
self.assertEqual(TbHypn0Item.objects.count(), 0) self.assertEqual(TbHypn0Item.objects.count(), 0)
@@ -522,11 +522,11 @@ class GalleryDetailAndDownloadTests(BaseMediaTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Астральный Транс Сознания #42") self.assertContains(response, "Астральный Транс Сознания #42")
self.assertContains(response, f"#{self.item.s_hash_id}") self.assertContains(response, f"#{self.item.s_hash_id}")
self.assertContains(response, "Векторная анатомия") self.assertContains(response, "Vector Anatomy")
self.assertContains(response, "Синтез психо-сетки") self.assertContains(response, "Psych-Grid Synthesis")
self.assertContains(response, "Астральный паспорт") self.assertContains(response, "Astral Passport")
self.assertContains(response, "Копировать SVG-код") self.assertContains(response, "Copy SVG code")
self.assertContains(response, "Скачать .svg") self.assertContains(response, "Download .svg")
# Проверка инкремента просмотров # Проверка инкремента просмотров
self.item.refresh_from_db() self.item.refresh_from_db()
@@ -557,7 +557,7 @@ class GalleryDetailAndDownloadTests(BaseMediaTestCase):
client = Client() client = Client()
response = client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id})) response = client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id}))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Подчинитесь воле Гипножабы!") self.assertContains(response, "Surrender to&nbsp;Hypnotoad!")
class GalleryRandomAndNavigationTests(BaseMediaTestCase): class GalleryRandomAndNavigationTests(BaseMediaTestCase):
@@ -699,8 +699,8 @@ class UnconsciousMatrixTests(BaseMediaTestCase):
item = self.items[0] item = self.items[0]
response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": item.s_hash_id})) response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": item.s_hash_id}))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Матрица бессознательного") self.assertContains(response, "Matrix of the Unconscious")
self.assertContains(response, "5 узлов") self.assertContains(response, "5 nodes")
self.assertContains(response, f"#{item.s_hash_id}") self.assertContains(response, f"#{item.s_hash_id}")
self.assertContains(response, "btn-nav-prev") self.assertContains(response, "btn-nav-prev")
self.assertContains(response, "btn-nav-next") self.assertContains(response, "btn-nav-next")
@@ -782,7 +782,7 @@ class GalleryFreshFloorTests(BaseMediaTestCase):
response = self.client.get(reverse("hypn0_site:index")) response = self.client.get(reverse("hypn0_site:index"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Плеск бессознательного") self.assertContains(response, "Incubator of&nbsp;the Unconscious")
self.assertContains(response, "FRESH STREAM") self.assertContains(response, "FRESH STREAM")
self.assertContains(response, f"gallery-card-{item.s_hash_id}") self.assertContains(response, f"gallery-card-{item.s_hash_id}")
self.assertContains(response, f"¤{item.s_hash_id}") self.assertContains(response, f"¤{item.s_hash_id}")
@@ -935,12 +935,12 @@ class GalleryArchiveTests(BaseMediaTestCase):
url = reverse("hypn0_site:gallery_archive") url = reverse("hypn0_site:gallery_archive")
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Галерея транса") self.assertContains(response, "Trance Gallery")
self.assertContains(response, "ARCHIVE") self.assertContains(response, "ARCHIVE")
self.assertContains(response, "Все волны") self.assertContains(response, "All waves")
self.assertContains(response, "Инкубатор") self.assertContains(response, "Incubator")
self.assertContains(response, "Одобрено") self.assertContains(response, "Approved")
self.assertContains(response, "Золотой фонд") self.assertContains(response, "Hall of&nbsp;Fame")
# Все 3 картины присутствуют в выдаче # Все 3 картины присутствуют в выдаче
self.assertContains(response, f"gallery-card-{self.item_fresh.s_hash_id}") self.assertContains(response, f"gallery-card-{self.item_fresh.s_hash_id}")
self.assertContains(response, f"gallery-card-{self.item_curated.s_hash_id}") self.assertContains(response, f"gallery-card-{self.item_curated.s_hash_id}")
@@ -1013,21 +1013,21 @@ class GalleryArchiveTests(BaseMediaTestCase):
resp_p1 = self.client.get(url, data={"floor": "all", "sort": "likes"}) resp_p1 = self.client.get(url, data={"floor": "all", "sort": "likes"})
self.assertEqual(resp_p1.status_code, 200) self.assertEqual(resp_p1.status_code, 200)
self.assertEqual(len(resp_p1.context["page_obj"]), 16) self.assertEqual(len(resp_p1.context["page_obj"]), 16)
self.assertContains(resp_p1, "Фаза 1 из 2") self.assertContains(resp_p1, "Phase 1 of&nbsp;2")
self.assertContains(resp_p1, "<title>Галерея транса — Каталог анимированных картин | HypnoSVG</title>") self.assertContains(resp_p1, "<title>Trance Gallery — Animated SVG Catalog | HypnoSVG</title>")
# Проверяем сохранение query-параметров в ссылках пагинатора # Проверяем сохранение query-параметров в ссылках пагинатора
self.assertContains(resp_p1, "floor=all&sort=likes&page=2") self.assertContains(resp_p1, "floor=all&sort=likes&page=2")
resp_p2 = self.client.get(url, data={"floor": "all", "sort": "likes", "page": 2}) resp_p2 = self.client.get(url, data={"floor": "all", "sort": "likes", "page": 2})
self.assertEqual(resp_p2.status_code, 200) self.assertEqual(resp_p2.status_code, 200)
self.assertEqual(len(resp_p2.context["page_obj"]), 2) self.assertEqual(len(resp_p2.context["page_obj"]), 2)
self.assertContains(resp_p2, "Фаза 2 из 2") self.assertContains(resp_p2, "Phase 2 of&nbsp;2")
self.assertContains(resp_p2, "<title>Галерея транса — Каталог анимированных картин (Фаза 2) | HypnoSVG</title>") self.assertContains(resp_p2, "<title>Trance Gallery — Animated SVG Catalog (Phase 2) | HypnoSVG</title>")
self.assertContains(resp_p2, "floor=all&sort=likes&page=1") self.assertContains(resp_p2, "floor=all&sort=likes&page=1")
# Проверяем Title при фильтрации по этажу # Проверяем Title при фильтрации по этажу
resp_curated = self.client.get(url, data={"floor": "curated"}) resp_curated = self.client.get(url, data={"floor": "curated"})
self.assertContains(resp_curated, "<title>Одобрено Мозговым Слизнем | HypnoSVG</title>") self.assertContains(resp_curated, "<title>Approved by Brain Slug | HypnoSVG</title>")
class RescoreCommandTests(BaseMediaTestCase): class RescoreCommandTests(BaseMediaTestCase):
@@ -1311,12 +1311,12 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# Проверяем, что отображается один из динамических заголовков секции # Проверяем, что отображается один из динамических заголовков секции
content = response.content.decode("utf-8") content = response.content.decode("utf-8")
possible_headings = ( possible_headings = (
"Сигналы из параллельного континуума", "Signals from&nbsp;Parallel Continuum",
"Психо-артефакты ноосферы", "Noosphere Psycho-Artifacts",
"Заметки астрального консилиума", "Astral Council Field Notes",
"Рефлекторные отклики мозговых слизней", "Reflexive Responses of&nbsp;Brain Slugs",
"Публикации из глубин подсознания", "Dispatches from&nbsp;Subconscious Depths",
"Материалы хроник гипноза", "Materials from&nbsp;Hypno Chronicles",
) )
self.assertTrue(any(heading in content for heading in possible_headings)) self.assertTrue(any(heading in content for heading in possible_headings))
self.assertContains(response, "Хроники гипно-транса: Урок 1") self.assertContains(response, "Хроники гипно-транса: Урок 1")
@@ -1345,7 +1345,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# Проверка 1-й страницы ленты блога # Проверка 1-й страницы ленты блога
response = self.client.get("/blog") response = self.client.get("/blog")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Хроники гипноза") self.assertContains(response, "Hypno Chronicles")
self.assertContains(response, "Тестовая депеша 11") self.assertContains(response, "Тестовая депеша 11")
self.assertNotContains(response, "Черновик ноосферы") self.assertNotContains(response, "Черновик ноосферы")
self.assertContains(response, "page=2") self.assertContains(response, "page=2")
@@ -1388,7 +1388,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
resp_parent = self.client.get(f"/blog/{series_parent.slug}") resp_parent = self.client.get(f"/blog/{series_parent.slug}")
self.assertEqual(resp_parent.status_code, 200) self.assertEqual(resp_parent.status_code, 200)
self.assertContains(resp_parent, "Цикл: Основы векторного гипноза") self.assertContains(resp_parent, "Цикл: Основы векторного гипноза")
self.assertContains(resp_parent, "Материалы и главы этого цикла") self.assertContains(resp_parent, "Chapters in This Series")
self.assertContains(resp_parent, "Глава 1: Первичная модуляция") self.assertContains(resp_parent, "Глава 1: Первичная модуляция")
self.assertContains(resp_parent, "Глава 2: Фазовый сдвиг матрицы") self.assertContains(resp_parent, "Глава 2: Фазовый сдвиг матрицы")
@@ -1396,31 +1396,31 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
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, "Part of&nbsp;the&nbsp;series:")
self.assertContains(resp_ch1, self.item.s_title) 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, f"/gallery/{self.item.s_hash_id}")
self.assertContains(resp_ch1, "Скачать SVG") self.assertContains(resp_ch1, "Download SVG")
self.assertContains(resp_ch1, "Вес:") self.assertContains(resp_ch1, "Weight:")
self.assertContains(resp_ch1, "Следующая глава цикла") self.assertContains(resp_ch1, "Next chapter in series")
self.assertContains(resp_ch1, "Глава 2: Фазовый сдвиг матрицы") self.assertContains(resp_ch1, "Глава 2: Фазовый сдвиг матрицы")
# Проверка хлебных крошек # Проверка хлебных крошек
self.assertContains(resp_ch1, 'aria-label="Хлебные крошки"') self.assertContains(resp_ch1, 'aria-label="Breadcrumbs"')
self.assertContains(resp_ch1, ch1.blog_nav_title) self.assertContains(resp_ch1, ch1.blog_nav_title)
self.assertContains(resp_ch1, "Глава 1 из 2") self.assertContains(resp_ch1, "Chapter 1 of 2")
self.assertContains(resp_ch1, 'aria-current="page"') self.assertContains(resp_ch1, 'aria-current="page"')
# Проверка JSON-LD BreadcrumbList # Проверка JSON-LD BreadcrumbList
self.assertContains(resp_ch1, '"@type": "BreadcrumbList"') self.assertContains(resp_ch1, '"@type": "BreadcrumbList"')
self.assertContains(resp_ch1, '"name": "Главная"') self.assertContains(resp_ch1, '"name": "Home"')
self.assertContains(resp_ch1, '"name": "Хроники гипноза"') self.assertContains(resp_ch1, '"name": "Hypno Chronicles"')
self.assertContains(resp_ch1, series_parent.s_title) 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)
self.assertContains(resp_ch2, "Предыдущая глава цикла") self.assertContains(resp_ch2, "Previous chapter in series")
self.assertContains(resp_ch2, "Глава 1: Первичная модуляция") self.assertContains(resp_ch2, "Глава 1: Первичная модуляция")
# 4. Проверка доступа по числовому ID # 4. Проверка доступа по числовому ID
@@ -1437,10 +1437,10 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
"""Проверка отдачи кастомной страницы 404 в режиме DEBUG=False.""" """Проверка отдачи кастомной страницы 404 в режиме DEBUG=False."""
response = self.client.get("/totally-non-existent-hypno-route-404") response = self.client.get("/totally-non-existent-hypno-route-404")
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertContains(response, "Гипноузел не&nbsp;обнаружен", status_code=404) self.assertContains(response, "Hypno-Node Not Found", status_code=404)
self.assertContains(response, "СБОЙ МАТРИЦЫ // КОД?&nbsp;404", status_code=404) self.assertContains(response, "MATRIX ANOMALY // CODE&nbsp;404", status_code=404)
self.assertContains(response, "return-btn", status_code=404) self.assertContains(response, "return-btn", status_code=404)
self.assertContains(response, "Стираем остатки несущес&shy;твующей страницы", status_code=404) self.assertContains(response, "Erasing traces of the phantom page", status_code=404)
@override_settings(DEBUG=True) @override_settings(DEBUG=True)
def test_debug_error_preview_routing(self): def test_debug_error_preview_routing(self):
@@ -1448,23 +1448,23 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# 1. Индексный список страниц ошибок # 1. Индексный список страниц ошибок
resp_list = self.client.get("/_error/") resp_list = self.client.get("/_error/")
self.assertEqual(resp_list.status_code, 200) self.assertEqual(resp_list.status_code, 200)
self.assertContains(resp_list, "Центр отладки страниц ошибок") self.assertContains(resp_list, "Error Pages Debug Center")
self.assertContains(resp_list, "/_error/404") self.assertContains(resp_list, "/_error/404")
self.assertContains(resp_list, "готов к просмотру") self.assertContains(resp_list, "ready to preview")
# 2. Проверка каждого кода ошибки # 2. Проверка каждого кода ошибки
expected_checks = [ expected_checks = [
("400", 400, "Сбой ментального протокола"), ("400", 400, "Perceptual Protocol Mismatch"),
("401", 401, "Требуется ментальная идентификация"), ("401", 401, "Subconscious Identity Required"),
("403", 403, "Отказано бюрократом 24-го уровня"), ("403", 403, "Cognitive Lockdown Active"),
("404", 404, "Гипноузел не&nbsp;обнаружен"), ("404", 404, "Hypno-Node Not Found"),
("413", 413, "Перегрузка сенсорных каналов"), ("413", 413, "Sensory Channel Saturation"),
("429", 429, "Слишком много мыслей в&nbsp;секунду"), ("429", 429, "Too Many Thoughts per Second"),
("500", 500, "Критический перегрев неокортекса"), ("500", 500, "Introspection Neural Lock"),
("502", 502, "Контейнер потерял сознание"), ("502", 502, "Uplink to Hippocampus Lost"),
("503", 503, "Плановый сеанс гипнотерапии"), ("503", 503, "Scheduled Hypnotherapy in Progress"),
("504", 504, "Разрыв астральной связи"), ("504", 504, "Astral Synapse Timed Out"),
("under_reconstruction", 200, "Отдел мозга на&nbsp;реконструкции"), ("under_reconstruction", 200, "Brain Sector Under Reconstruction"),
] ]
for code, expected_status, text_fragment in expected_checks: for code, expected_status, text_fragment in expected_checks:
@@ -1478,12 +1478,12 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# 3. Предпросмотр с суффиксом .html # 3. Предпросмотр с суффиксом .html
resp_404_html = self.client.get("/_error/404.html") resp_404_html = self.client.get("/_error/404.html")
self.assertEqual(resp_404_html.status_code, 404) self.assertEqual(resp_404_html.status_code, 404)
self.assertContains(resp_404_html, "Гипноузел не&nbsp;обнаружен", status_code=404) self.assertContains(resp_404_html, "Hypno-Node Not Found", status_code=404)
# 4. Несуществующий шаблон ошибки в dev-режиме # 4. Несуществующий шаблон ошибки в dev-режиме
resp_non_existent = self.client.get("/_error/non-existent-code-999") resp_non_existent = self.client.get("/_error/non-existent-code-999")
self.assertEqual(resp_non_existent.status_code, 404) self.assertEqual(resp_non_existent.status_code, 404)
self.assertContains(resp_non_existent, "Шаблон для ошибки 'non-existent-code-999' не найден", status_code=404) self.assertContains(resp_non_existent, "Template for error 'non-existent-code-999' not found", status_code=404)
def test_custom_django_error_handlers(self): def test_custom_django_error_handlers(self):
"""Проверка прямого вызова стандартных Django-обработчиков ошибок.""" """Проверка прямого вызова стандартных Django-обработчиков ошибок."""
@@ -1495,22 +1495,22 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
req400 = factory.get("/bad-request") req400 = factory.get("/bad-request")
resp400 = error_400(req400) resp400 = error_400(req400)
self.assertEqual(resp400.status_code, 400) self.assertEqual(resp400.status_code, 400)
self.assertIn("Сбой ментального протокола", resp400.content.decode("utf-8")) self.assertIn("Perceptual Protocol Mismatch", resp400.content.decode("utf-8"))
req403 = factory.get("/forbidden") req403 = factory.get("/forbidden")
resp403 = error_403(req403) resp403 = error_403(req403)
self.assertEqual(resp403.status_code, 403) self.assertEqual(resp403.status_code, 403)
self.assertIn("Отказано бюрократом 24-го уровня", resp403.content.decode("utf-8")) self.assertIn("Cognitive Lockdown Active", resp403.content.decode("utf-8"))
req404 = factory.get("/not-found") req404 = factory.get("/not-found")
resp404 = error_404(req404) resp404 = error_404(req404)
self.assertEqual(resp404.status_code, 404) self.assertEqual(resp404.status_code, 404)
self.assertIn("Гипноузел не&nbsp;обнаружен", resp404.content.decode("utf-8")) self.assertIn("Hypno-Node Not Found", resp404.content.decode("utf-8"))
req500 = factory.get("/server-error") req500 = factory.get("/server-error")
resp500 = error_500(req500) resp500 = error_500(req500)
self.assertEqual(resp500.status_code, 500) self.assertEqual(resp500.status_code, 500)
self.assertIn("Критический перегрев неокортекса", resp500.content.decode("utf-8")) self.assertIn("Introspection Neural Lock", resp500.content.decode("utf-8"))
def test_s_title_plain_property(self): def test_s_title_plain_property(self):
"""Проверка очистки HTML-тегов и мнемоник в s_title_plain для моделей TbHypn0Item и TbBlogPost.""" """Проверка очистки HTML-тегов и мнемоник в s_title_plain для моделей TbHypn0Item и TbBlogPost."""
+10 -10
View File
@@ -196,14 +196,14 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
break break
except TemplateDoesNotExist: except TemplateDoesNotExist:
pass pass
status_mark = "готов к просмотру" if exists else "шаблон не создан" status_mark = "ready to preview" if exists else "template not found"
links.append(f"<li><a href='/_error/{c}'><strong>/{c}</strong></a> ({status_mark})</li>") links.append(f"<li><a href='/_error/{c}'><strong>/{c}</strong></a> ({status_mark})</li>")
html = f"""<!doctype html> html = f"""<!doctype html>
<html lang="ru"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Dev: Предпросмотр страниц ошибок // HypnoSVG</title> <title>Dev: Error Pages Preview // HypnoSVG</title>
<style> <style>
body {{ font-family: ui-monospace, monospace; background: #09090b; color: #d9f99d; padding: 2rem; line-height: 1.6; }} 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 {{ color: #a3e635; text-decoration: none; border-bottom: 1px dotted #a3e635; }}
@@ -215,8 +215,8 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<h2>Центр отладки страниц ошибок (DEBUG=True)</h2> <h2>Error Pages Debug Center (DEBUG=True)</h2>
<p>Выберите код для инспекции верстки и интерактивных скриптов перекалибровки:</p> <p>Select an error code to inspect layout and recalibration scripts:</p>
<ul> <ul>
{''.join(links)} {''.join(links)}
</ul> </ul>
@@ -244,7 +244,7 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
if not found_template: if not found_template:
return HttpResponse( return HttpResponse(
f"<h1>Шаблон для ошибки '{raw_name}' не найден</h1><p>Проверены: {', '.join(template_candidates)}</p><p><a href='/_error/'>← Вернуться к списку</a></p>", f"<h1>Template for error '{raw_name}' not found</h1><p>Checked: {', '.join(template_candidates)}</p><p><a href='/_error/'>← Back to index</a></p>",
status=404, status=404,
content_type="text/html; charset=utf-8", content_type="text/html; charset=utf-8",
) )
@@ -281,7 +281,7 @@ def generate(request: HttpRequest) -> HttpResponse:
form = HalftoneGenerateForm(request.POST, request.FILES) form = HalftoneGenerateForm(request.POST, request.FILES)
if not form.is_valid(): if not form.is_valid():
error_msg = next(iter(form.errors.values()))[0] if form.errors else "Ошибка параметров генерации" error_msg = next(iter(form.errors.values()))[0] if form.errors else "Generation parameter error"
return render(request, "block/preview.html", {"error": error_msg}) return render(request, "block/preview.html", {"error": error_msg})
image_file = form.cleaned_data["image"] image_file = form.cleaned_data["image"]
@@ -313,7 +313,7 @@ def generate(request: HttpRequest) -> HttpResponse:
return render( return render(
request, request,
"block/preview.html", "block/preview.html",
{"error": f"Сбой в матрице гипноза: {str(e)}"}, {"error": f"Glitch in hypno matrix: {str(e)}"},
) )
return render( return render(
@@ -338,7 +338,7 @@ def publish(request: HttpRequest) -> HttpResponse:
return render( return render(
request, request,
"block/publish_status.html", "block/publish_status.html",
{"error": "Нет данных SVG для публикации. Попробуйте сгенерировать заново."}, {"error": "No SVG data for publication. Please try generating again."},
) )
visitor_uuid = request.COOKIES.get("hypn0_vid") visitor_uuid = request.COOKIES.get("hypn0_vid")
@@ -411,7 +411,7 @@ def publish(request: HttpRequest) -> HttpResponse:
return render( return render(
request, request,
"block/publish_status.html", "block/publish_status.html",
{"error": f"Сбой фиксации в трансе: {str(e)}"}, {"error": f"Failed to record in trance: {str(e)}"},
) )
return render( return render(
+1 -1
View File
@@ -11,7 +11,7 @@
{# ПРАВОЕ МЕНЮ #}<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"> {# ПРАВОЕ МЕНЮ #}<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-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">gallery</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">gallery</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">blog</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">blog</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="Light/Dark theme"><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 border-0 text-slate-900 dark:text-slate-100" @click.prevent="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)" title="Light/Dark theme"><span x-text="darkMode?'●':'○'"></span></a>
</menu>{# /ПРАВОЕ МЕНЮ #} </menu>{# /ПРАВОЕ МЕНЮ #}
</div> </div>
</header> </header>
+172 -22
View File
@@ -5,11 +5,11 @@
{% block META_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 %}Dispatch from the Hypno Chronicles of HypnoSVG: {{ post.s_title|striptags }}{% endif %}{% endblock %} {% block META_DESCRIPTION %}{% if post.s_teaser %}{{ post.s_teaser|striptags|truncatechars:"160" }}{% else %}Dispatch from the Hypno Chronicles of HypnoSVG: {{ post.s_title|striptags }}{% endif %}{% endblock %}
{% block OG_TITLE %}{{ post.s_title|striptags }} | HypnoSVG{% endblock %} {% block OG_TITLE %}{{ post.s_title|striptags }} | HypnoSVG{% endblock %}
{% block OG_DESCRIPTION %}{% if post.s_teaser %}{{ post.s_teaser|striptags|truncatechars:200 }}{% else %}Dispatch from the Hypno Chronicles of HypnoSVG: {{ post.s_title|striptags }}{% endif %}{% endblock %} {% block OG_DESCRIPTION %}{% if post.s_teaser %}{{ post.s_teaser|striptags|truncatechars:"200" }}{% else %}Dispatch from the Hypno Chronicles of 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 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 %}
@@ -57,8 +57,8 @@
"@type": "Article", "@type": "Article",
"headline": "{{ post.s_title|striptags|escapejs }}", "headline": "{{ post.s_title|striptags|escapejs }}",
"description": "{{ post.s_teaser|striptags|escapejs }}", "description": "{{ post.s_teaser|striptags|escapejs }}",
"datePublished": "{{ post.d_published_at|date:'r' }}", "datePublished": "{{ post.d_published_at|date:"r" }}",
"dateModified": "{{ post.d_updated_at|date:'r' }}", "dateModified": "{{ post.d_updated_at|date:"r" }}",
"mainEntityOfPage": { "mainEntityOfPage": {
"@type": "WebPage", "@type": "WebPage",
"@id": "{{ request.scheme }}://{{ request.get_host }}{{ post.get_absolute_url }}" "@id": "{{ request.scheme }}://{{ request.get_host }}{{ post.get_absolute_url }}"
@@ -116,27 +116,21 @@
</div> </div>
{% endif %} {% endif %}
<!-- ЕДИНАЯ ДВУХКОЛОНОЧНАЯ СЕТКА: 1/4 слева (превью + SVG), 3/4 справа (заголовок + дата + тизер + контент) --> {% if post.f_cover_img and post.k_item %}
<!-- ДВА БЛОКА: 1. Верхний ряд (обложка слева + заголовок/тизер справа) и 2. Нижний ряд (SVG/экспликация слева + контент справа) -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-x-6 items-start"> <div class="grid grid-cols-1 md:grid-cols-4 gap-x-6 items-start">
<!-- 1. Верхний ряд слева (1/4): Тизер-превью обложки --> <!-- 1. Верхний ряд слева (1/4): Тизер-превью обложки -->
<div class="md:col-span-1 w-full pb-8"> <div class="order-2 md:order-1 md:col-span-1 w-full pb-8">
{% if post.f_cover_img %}{# ЕСТЬ ТИЗЕР-КАРТИНКА И ПОКАЗЫВАЕМ ЕË #} <div class="aspect-square rounded-2xl overflow-hidden border border-stone-200 dark:border-zinc-800 shadow-md bg-stone-100 dark:bg-zinc-900">
<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 }}" <img src="{{ post.f_cover_img.url }}"
alt="{{ post.s_title|striptags }}" alt="{{ post.s_title|striptags }}"
class="w-full h-full object-cover"> class="w-full h-full object-cover">
</div> </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="Brain Slug" class="w-10 h-10 opacity-40 mb-1">{# <-- СЮДА ДЕФОЛТНУЮ КАРТИНКУ #}
<span class="text-[10px] uppercase tracking-wider">No visual transmitted</span>{# <-- А ПОСЛЕ ЭТО УБРАТЬ #}
</div>
{% endif %}
</div> </div>
<!-- 1. Верхний ряд справа (3/4): Заголовок + Дата + Тизер с непрерывной пунктирной линией --> <!-- 1. Верхний ряд справа (3/4): Заголовок + Дата + Тизер с непрерывной пунктирной линией -->
<div class="md:col-span-3 border-l-1 border-purple-500 border-dotted pl-6 pb-8 space-y-4"> <div class="order-1 md:order-2 md:col-span-3 border-l-0 md:border-l-1 border-purple-500 border-dotted pl-0 md: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"> <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 }} {{ post.s_title|safe }}
</h1> </h1>
@@ -161,12 +155,11 @@
{% endif %} {% endif %}
</div> </div>
<!-- 2. Нижний ряд слева (1/4): Связанный SVG + Музейная экспликация (выровнены по вертикали с первой строкой контента) --> <!-- 2. Нижний ряд слева (1/4): Связанный SVG + Музейная экспликация под ним -->
<div class="md:col-span-1 space-y-4"> <div class="order-2 md:order-1 md:col-span-1 space-y-4 pt-6 md:pt-0">
{% if post.k_item %}
<aside class="space-y-3 md:sticky md:top-6"> <aside class="space-y-3 md:sticky md:top-6">
<!-- Интерактивное SVG-полотно экспоната --> <!-- Интерактивное 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"> <div class="aspect-square rounded-2xl overflow-hidden bg-[var(--card-bg-light)] dark:bg-[var(--card-bg-dark)] border border-stone-300/80 dark:border-zinc-800/80 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 %} {% if active_svg %}
<div class="w-full h-full flex items-center justify-center pointer-events-auto"> <div class="w-full h-full flex items-center justify-center pointer-events-auto">
<template shadowrootmode="open"> <template shadowrootmode="open">
@@ -265,11 +258,10 @@
</div> </div>
</div> </div>
</aside> </aside>
{% endif %}
</div> </div>
<!-- 2. Нижний ряд справа (3/4): Текстовый блок статьи --> <!-- 2. Нижний ряд справа (3/4): Текстовый блок статьи -->
<div class="md:col-span-3 border-l-1 border-purple-500 border-dotted pl-6"> <div class="order-1 md:order-2 md:col-span-3 border-l-0 md:border-l-1 border-purple-500 border-dotted pl-0 md:pl-6">
<div class="prose max-w-none font-sans text-base sm:text-lg"> <div class="prose max-w-none font-sans text-base sm:text-lg">
{{ post.s_content|safe }} {{ post.s_content|safe }}
</div> </div>
@@ -277,6 +269,164 @@
</div> </div>
{% else %}
<!-- ЕДИНЫЙ БЛОК: Левая колонка (SVG/экспликация, либо обложка, либо заглушка) + Правая колонка (заголовок + дата + тизер + контент) -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-x-6 items-start">
<!-- Левая колонка (1/4) -->
<div class="order-2 md:order-1 md:col-span-1 space-y-4 pt-6 md:pt-0">
{% if post.k_item %}{# 1. ЕСТЬ SVG (И НЕТ ОБЛОЖКИ) — ПОКАЗЫВАЕМ SVG И ПОД НИМ ЭКСПЛИКАЦИЮ #}
<aside class="space-y-3 md:sticky md:top-6">
<!-- Интерактивное SVG-полотно экспоната -->
<div class="aspect-square rounded-2xl overflow-hidden bg-[var(--card-bg-light)] dark:bg-[var(--card-bg-dark)] border border-stone-300/80 dark:border-zinc-800/80 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="View artwork in gallery">#{{ 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">Oscillators:</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">Weight:</span>
<span class="font-mono text-stone-900 dark:text-stone-200">{{ svg_stats.total_kb }} 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>Download SVG</span>
</a>
</div>
</div>
</aside>
{% elif post.f_cover_img %}{# 2. ЕСТЬ ТОЛЬКО ОБЛОЖКА #}
<div class="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 %}{# 3. НЕТ НИ SVG, НИ ОБЛОЖКИ — ДЕФОЛТНАЯ ЗАГЛУШКА #}
<div class="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="Brain Slug" class="w-10 h-10 opacity-40 mb-1">
<span class="text-[10px] uppercase tracking-wider">No visual transmitted</span>
</div>
{% endif %}
</div>
<!-- Правая колонка (3/4): Заголовок + Дата + Тизер + Контент -->
<div class="order-1 md:order-2 md:col-span-3 border-l-0 md:border-l-1 border-purple-500 border-dotted pl-0 md:pl-6 space-y-6">
<div class="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:"r" }}
</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>
<!-- Основной текст статьи -->
<div class="prose max-w-none font-sans text-base sm:text-lg">
{{ post.s_content|safe }}
</div>
</div>
</div>
{% endif %}
<!-- РАЗДЕЛ СЕРИИ: список глав (если статья является родительской подборкой/серией) --> <!-- РАЗДЕЛ СЕРИИ: список глав (если статья является родительской подборкой/серией) -->
{% if series_posts and not series_parent %} {% if series_posts and not series_parent %}
<section class="space-y-4 pt-8 border-t border-stone-200 dark:border-zinc-800"> <section class="space-y-4 pt-8 border-t border-stone-200 dark:border-zinc-800">
@@ -292,7 +442,7 @@
<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="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"> <div class="flex items-center justify-between text-[11px] text-stone-400">
<span class="font-bold text-purple-600 dark:text-purple-400">Chapter {{ child.i_order }}</span> <span class="font-bold text-purple-600 dark:text-purple-400">Chapter {{ child.i_order }}</span>
<time datetime="{{ child.d_published_at|date:"c" }}">{{ child.d_published_at|date:'r' }}</time> <time datetime="{{ child.d_published_at|date:"c" }}">{{ child.d_published_at|date:"r" }}</time>
</div> </div>
<h3 class="font-bold text-sm text-stone-900 dark:text-stone-100"> <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"> <a href="{{ child.get_absolute_url }}" class="hover:text-purple-600 dark:hover:text-purple-400 transition-colors border-0">
+150 -64
View File
@@ -1,69 +1,155 @@
<!DOCTYPE html> {% extends '_base.html' %}
<html lang="en"> {% load static %}
<head>
<meta charset="UTF-8">
<title>Security &amp; Privacy Policy — HypnoSVG</title>
</head>
<body>
TEMPORARY DRAFT
<h1>Security &amp; Privacy Policy (SECURITY)</h1> {% block PAGE_TITLE %}About HypnoSVG (hypn0) — Procedural Animated Halftone & Vector Matrix Engine{% endblock %}
—teaser: {% block META_TITLE %}About HypnoSVG — Production-Ready Animated Halftones & Vector Matrices{% endblock %}
<p>We&nbsp;don’t know who you are…&nbsp;and&nbsp;we&nbsp;don’t want to&nbsp;know. The&nbsp;Hypn0.xyz (HypnoSVG) platform operates in&nbsp;strict compliance with&nbsp;Inter&shy;galactic DOOP Protocols and&nbsp;GDPR directives, adhering to&nbsp;the&nbsp;Zero Personally Identifiable Information (Zero-PII) doctrine. No&nbsp;registration, logins, passwords, emails, SMS, or&nbsp;phone numbers. The&nbsp;database contains zero user or&nbsp;account tables. Synthesized SVG files contain purely mathematical coordinates of&nbsp;vector primitives and&nbsp;inline CSS animations. No&nbsp;source image metadata is&nbsp;embedded (EXIF tags, geolocation, and&nbsp;hardware device profiles are purged during raster&shy;ization).</p> {% block META_DESCRIPTION %}Learn how HypnoSVG turns raster graphics into lightweight, hypnotic animated vector matrices. Practical use cases for web design, banner backdrops, hero typography, open-source repository, and custom bespoke engineering.{% endblock %}
<h2>Prologue: The&nbsp;Anatomy of&nbsp;Privacy in&nbsp;the&nbsp;Age of&nbsp;Mental Resonance</h2>
<p><em>We are the&nbsp;Hypnotoad, the&nbsp;Brain Slugs, and the&nbsp;Receptors of&nbsp;Planetary Mind-Numbing. And we don’t want to&nbsp;know who you are. Don’t even try to&nbsp;persuade us.</em></p>
<p>The&nbsp;Hypn0 platform is&nbsp;engineered from&nbsp;the&nbsp;ground up on&nbsp;the&nbsp;<strong>Zero-PII (Zero Personally Identifiable Information)</strong> doctrine. We&nbsp;do not&nbsp;require registration, logins, passwords, email addresses, or&nbsp;phone numbers. In&nbsp;the&nbsp;project database, user and&nbsp;account tables simply do not&nbsp;exist. Generated SVG files contain only mathematical coordinates of&nbsp;geometric primitives and&nbsp;inline CSS keyframes. Zero metadata from&nbsp;source images is&nbsp;retained (EXIF tags, GPS coordinates, device specs, and&nbsp;even source file names are completely stripped during raster&shy;ization).</p>
<hr/>
<h2>Section&nbsp;I.&nbsp;No-Consent Mode (Default / No-Tracking)</h2>
<p>If&nbsp;you have not&nbsp;clicked the&nbsp;“<strong>Submit to&nbsp;Hypnotoad!</strong>” button—you are completely invisible in&nbsp;the&nbsp;astral realm. This&nbsp;means:</p>
<ol>
<li>Hypn0 (HypnoSVG) does not&nbsp;deploy <strong>any client identifiers</strong>. Neither cookies nor browser <code>localStorage</code> records are created.</li>
<li><strong>External analytics systems are disabled</strong>: tracking beacons and metric counters (Google Tag Manager, analytics scripts, etc.) are blocked at&nbsp;the&nbsp;template level and&nbsp;physically never loaded onto the&nbsp;client device.</li>
<li><strong>Server logs</strong>—the&nbsp;only baseline telemetry captured by&nbsp;infrast&shy;ructure (IP address, User-Agent, requested URL, and&nbsp;timestamp) — are utilized strictly for&nbsp;DDoS defense and&nbsp;uptime monitoring, retained for&nbsp;a&nbsp;minimal duration with&nbsp;regular rotation faster than memories fade after a&nbsp;hypnosis session.</li>
</ol>
<hr/>
<h2>Section II.&nbsp;Restrictions for&nbsp;Free Humanoids Without a&nbsp;Brain Slug</h2>
<p>Humanoids who decline tracking or&nbsp;withhold explicit consent can freely use the&nbsp;local vector generator and&nbsp;browse the&nbsp;gallery. However, they&nbsp;<strong>cannot access pseudo-social platform features</strong>:</p>
<ul>
<li>Publishing original works to&nbsp;the&nbsp;public “<a href="/gallery">Trance Gallery</a>”;</li>
<li>Voting for&nbsp;artworks (transmitting psychic resonance / likes);</li>
<li>Submitting complaints or&nbsp;flagging spam and&nbsp;non-transcendent content.</li>
</ul>
<h3>Reason for&nbsp;Restrictions: Anti-Abuse &amp; Bot Defense</h3>
<p>To&nbsp;operate the&nbsp;gravit&shy;ational Brain Slug sorting algorithm (curated ranking) and&nbsp;fairly distribute artworks across tiers (“Incubator”, “Approved by&nbsp;Brain Slug”, “Deep Trance Hall of&nbsp;Fame”), the&nbsp;platform requires a&nbsp;unique anonymous client anchor.</p>
<p>Because a&nbsp;humanoid who has not&nbsp;“surrendered to&nbsp;the&nbsp;Hypnotoad” carries no&nbsp;device anchor, the&nbsp;Lord of&nbsp;Mental Intros&shy;pections (the&nbsp;server) cannot recognize duplicate votes. This&nbsp;would lead to&nbsp;instant scripted vote tampering, spam surges, and&nbsp;the&nbsp;destabi&shy;lization of&nbsp;the&nbsp;entire gallery ecosystem.</p>
<hr/>
<h2>Section III.&nbsp;Consent Mechanism via “Submit to&nbsp;Hypnotoad!”</h2>
<ol>
<li>The&nbsp;browser generates a&nbsp;cryptog&shy;raphic pseudo-random identifier via <code>crypto.randomUUID()</code> (UUID v4).</li>
<li>The&nbsp;value is&nbsp;stored in&nbsp;a&nbsp;cookie and/or&nbsp;browser <code>localStorage</code> under the&nbsp;key <code>hypn0_vid</code>.</li>
<li>The&nbsp;lifespan is&nbsp;configured for&nbsp;two Earth years, with&nbsp;transmission flags set to&nbsp;safe mode (<code>path=/; SameSite=Lax</code>).</li>
<li>The&nbsp;<code>hypn0_vid</code> token is&nbsp;a&nbsp;purely random string and&nbsp;cannot be linked to&nbsp;your real identity, physical vessel, or&nbsp;cerebral Theta/Alpha brainwaves.</li>
<li>The&nbsp;identifier is&nbsp;used by&nbsp;the&nbsp;backend exclusively for&nbsp;rate limiting and&nbsp;enforcing the&nbsp;“one vote per artwork per browser” rule.</li>
</ol>
<hr/>
<h2>Section IV.&nbsp;File Processing Security during SVG Generation</h2>
<ul>
<li><strong>Input Data Sanitization (Source Images):</strong> All inbound images pass through the&nbsp;Pillow mental shredder, decoded exclusively in&nbsp;RAM, and&nbsp;immediately cleansed of&nbsp;mundane metadata. No&nbsp;EXIF tags, geolocation, camera models, or&nbsp;temporary disk artifacts remain on&nbsp;server storage.</li>
<li><strong>Output Vector Security:</strong> The&nbsp;server stores only vector artworks synthesized by&nbsp;the&nbsp;native Hypn0 (HypnoSVG) core. Output SVGs are generated strictly program&shy;matically and&nbsp;contain no&nbsp;<code>&lt;script&gt;</code> tags, external <code>xlink:href</code> links, or&nbsp;event handlers like <code>onload</code> or&nbsp;<code>onclick</code>. This&nbsp;eliminates Stored XSS risks when&nbsp;viewing other humanoids’ works: foreign halftones are completely harmless to&nbsp;your optic nerves (aside from&nbsp;the&nbsp;risk of&nbsp;spontaneous hypnotic trance).</li>
<li><strong>Resource Limits (DoS Prevention):</strong> Strict thresholds are?&nbsp;enforced on&nbsp;maximum upload size, grid density (<code>cols</code>), and&nbsp;element radii, preventing memory exhaustion and&nbsp;overheating of&nbsp;New New York processors.</li>
</ul>
<hr/>
<h2>Epilogue: Matrix Anomalies &amp; Vulner&shy;ability Disclosure</h2>
<p>If&nbsp;you discover a&nbsp;security flaw capable of&nbsp;bypassing the&nbsp;will of&nbsp;the&nbsp;Hypnotoad or&nbsp;injecting alien code into&nbsp;the&nbsp;consciousness of&nbsp;Hypn0 — please approach disclosure with&nbsp;utmost inter&shy;galactic respon&shy;sibility:</p>
<ul>
<li>Do not&nbsp;publish vulner&shy;ability details publicly, preventing space pirates and&nbsp;rogue bots from&nbsp;exploiting them before a&nbsp;patch is&nbsp;deployed.</li>
<li>Send a&nbsp;detailed report to&nbsp;the&nbsp;project maintainer via the&nbsp;<a href="https://github.com/erjemin/2026-hypn0-site/issues" target="_blank">repository issue tracker</a> marked as&nbsp;security, or&nbsp;directly to&nbsp;the&nbsp;<a href="#" id="c-l" onclick="this.href='ma'+atob('aWx0bzplcmplb'+'WluQGdtYWlsLmNvbQ==');">admin&shy;istrator’s email</a>.</li>
<li>Describe attack vectors, step-by-step reproduction (PoC), and&nbsp;potential system impact.</li>
<li>We will promptly acknowledge receipt and&nbsp;neutralize the&nbsp;anomaly. Please refrain from&nbsp;public disclosure until the&nbsp;vulner&shy;ability is&nbsp;resolved.</li>
</ul>
<hr/> {% block OG_TITLE %}About HypnoSVG — Procedural Animated Vector Halftone Engine{% endblock %}
<hr/>
<hr/>
<hr/>
</body> {% block OG_DESCRIPTION %}Deconstruct raster images into pulsating CSS-animated SVG matrices. Production-ready, ultra-lightweight, open-source, and zero JS execution overhead.{% endblock %}
</html>
{% block CANONICAL %}{{ request.scheme }}://{{ request.get_host }}/about/{% endblock %}
{% block CONTENT %}
<article class="prose max-w-7xl mx-auto py-4 font-sans text-stone-800 dark:text-stone-200">
<!-- Header -->
<header>
<p><em>About the Platform &amp; Practical Utility</em></p>
<h1>Vector Halftones That Refuse to Be Ignored</h1>
<p>
<strong>HypnoSVG (hypn0)</strong> is an open-source procedural raster-to-vector engine that transforms ordinary photos, logos, and digital graphics into pulsating, mathematically synchronized SVG matrices with zero JavaScript runtime overhead.
</p>
</header>
<hr>
<!-- Core Proposition -->
<section>
<h2>What Is HypnoSVG and Why Does It Exist?</h2>
<p>
Modern web design is saturated with static flat illustrations and bloated, power-hungry video backgrounds. GIF animations are heavy and pixelated, while canvas-based WebGL shaders often spike mobile CPU load and drain battery life.
</p>
<p>
<strong>HypnoSVG</strong> bridges the gap by decomposing raster images into procedural geometric grids (circles, concentric rings, diamonds, stars, binary code, snowflakes, and custom glyphs) driven by native, phase-shifted <strong>pure CSS <code>@keyframes</code></strong>.
</p>
<ul>
<li><strong>Ultra-Lightweight (20–50 KB):</strong> Clean vector markup that scales infinitely from a mobile favicon to a 4K billboard without pixelation.</li>
<li><strong>Zero Scripting Overhead:</strong> 100% hardware-accelerated browser rendering with zero JS dependencies.</li>
<li><strong>CC0 1.0 Public Domain Output:</strong> All generated vector files are completely free for personal and commercial use without mandatory attribution.</li>
<li><strong>Open-Source Core:</strong> The source code is open and available on <a href="https://git.cube2.ru/erjemin/2026-hypn0-site" target="_blank" rel="noopener">git.cube2.ru</a> and mirrored on <a href="https://github.com/erjemin/2026-hypn0-site" target="_blank" rel="noopener">GitHub</a> under the PolyForm Noncommercial License.</li>
</ul>
</section>
<hr>
<!-- Practical Use Cases -->
<section>
<h2>Practical Ways to Use HypnoSVG in Production</h2>
<p>
While our aesthetic draws subtle inspiration from the hypnotic frequencies of the legendary Hypnotoad, the generated output is engineered strictly for real-world web design, UI/UX, marketing, and creative engineering:
</p>
<h3>1. High-Conversion Banner Backdrops (Defeating Banner Blindness)</h3>
<p>
Standard static display ads and promotional blocks often get filtered out by human peripheral vision. An animated HypnoSVG matrix introduces subtle, rhythmic micro-pulsation that naturally draws attention without being intrusive.
</p>
<blockquote>
<p>
<strong>Pro-Tip for Text Legibility:</strong> Inside the generated SVG (or via its CSS container), adjust the overall opacity to <code>opacity: 0.15–0.35</code> and place it over a solid or slightly tinted background. Any promotional typography, CTA buttons, or pricing tags placed on top will remain <strong>100% crisp and readable</strong>, while the living matrix underneath delivers an unmistakable dynamic depth.
</p>
</blockquote>
<h3>2. Hero Sections &amp; Knockout "Cut-Out" Typography</h3>
<p>
For tech startups, SaaS intros, and modern landing pages, contrast is essential. Invert the visual hierarchy: generate a rich, high-density dark or neon duotone halftone matrix, and overlay heavy white "knockout" text (reverse typography) across the viewport.
</p>
<p>
The kinetic motion within the matrix breathes vitality through the negative space of the letters, giving your headline an organic, almost living presence.
</p>
<h3>3. UI Atmospheric Layers, 404 Backdrops &amp; Interactive Preloaders</h3>
<p>
Embed animated SVGs directly inline or as CSS <code>background-image</code> rules. They work seamlessly as ambient background textures for Dark Mode interfaces, cyberpunk dashboards, radar HUDs, or error screens that turn a standard 404 into an engaging brand touchpoint.
</p>
<h3>4. Merchandise, Posters &amp; CNC / Laser Fabrication</h3>
<p>
When paused or downloaded as static vector paths, HypnoSVG files are ready for screen printing, vinyl plotting, laser engraving, and silk-screen apparel design.
</p>
</section>
<hr>
<!-- Online Generator Limits & Bespoke Customization -->
<section>
<h2>Online Generator Limits &amp; Custom Commercial Projects</h2>
<p>
The public online generator at <a href="{% url 'hypn0_site:index' %}">hypn0.xyz</a> is tuned for instant browser-based responsiveness and shared server stability, with reasonable limits on upload dimensions, grid density, and element counts.
</p>
<p>
Need something beyond standard online capabilities? The author is available for <strong>custom development and bespoke vector engineering</strong>:
</p>
<ul>
<li>High-density ultra-resolution offline generations (billboards, high-DPI prints, complex multi-layer vector matrices).</li>
<li>Engine modifications, custom algorithmic shaders, specialized geometric primitives, and branded SVG patterns.</li>
<li>Tailored frontend integrations for interactive websites, dynamic data visualizations, and bespoke design systems.</li>
</ul>
<p>
To discuss bespoke commissions, engine licensing, or technical collaboration, reach out to the <a href="#" id="c-l" onclick="this.href='ma'+atob('aWx0bzplcmplb'+'WluQGdtYWlsLmNvbQ==')+'?subject='+encodeURIComponent('HypnoSVG: Bespoke Project / Custom Commission');">administrator’s email</a>.
</p>
</section>
<hr>
<!-- Community Showcase & Blog Features -->
<section>
<h2>Community Showcase: Get Featured on Hypno Chronicles</h2>
<p>
We actively highlight real-world use cases, design experiments, and production deployments in our official blog (<strong>Hypno Chronicles</strong>).
</p>
<p>
Have you used HypnoSVG in a live website, landing page, advertising campaign, poster series, album cover, or generative coding project? Send us your case study!
</p>
<ul>
<li><strong>Case submission:</strong> Send your project description, live URL, and accompanying illustrations or screenshots.</li>
<li><strong>Curated selection:</strong> We do not publish automated submissions blindly, but the more interesting your project and the more detailed the write-up, the closer you get to a <strong>100% guarantee of publication</strong>.</li>
<li><strong>Backlinks &amp; Visibility:</strong> Published articles feature an in-depth breakdown of your workflow with direct, prominent links to your portfolio, agency, or social profiles, plus potential induction into the <a href="----">Trance Gallery Hall of Fame</a>.</li>
</ul>
<p>
Send your showcase submissions via <a href="https://github.com/erjemin/2026-hypn0-site/issues" target="_blank" rel="noopener">GitHub Issues</a> or directly via the <a href="#" id="c-l-showcase" onclick="this.href='ma'+atob('aWx0bzplcmplb'+'WluQGdtYWlsLmNvbQ==')+'?subject='+encodeURIComponent('HypnoSVG: Showcase Case Study Submission');">administrator’s email</a>.
</p>
</section>
<hr>
<!-- Open Source & Licensing -->
<section>
<h2>Open Source &amp; Licensing</h2>
<p>
HypnoSVG is developed with transparency and respect for creative independence:
</p>
<ul>
<li><strong>Source Repository:</strong> Hosted at <a href="https://git.cube2.ru/erjemin/2026-hypn0-site" target="_blank" rel="noopener">git.cube2.ru/erjemin/2026-hypn0-site</a> and mirrored on <a href="https://github.com/erjemin/2026-hypn0-site" target="_blank" rel="noopener">GitHub</a>.</li>
<li><strong>Platform Code:</strong> Licensed under <a href="https://git.cube2.ru/erjemin/2026-hypn0-site/src/branch/master/LICENSE.md" target="_blank" rel="noopener">PolyForm Noncommercial 1.0.0</a>.</li>
<li><strong>Synthesized SVGs:</strong> Released under <a href="https://creativecommons.org/publicdomain/zero/1.0/" target="_blank" rel="noopener">Creative Commons Zero (CC0 1.0 Universal)</a> — completely unrestricted for personal and commercial usage.</li>
</ul>
</section>
<hr>
<!-- Footer Navigation -->
<footer>
<p>
<a href="{% url 'hypn0_site:index' %}">Launch Generator &rarr;</a> |
<a href="----">Explore Gallery &rarr;</a> |
<a href="{% url 'hypn0_site:blog_feed' %}">Read Blog &rarr;</a>
</p>
<p><small>All glory to the Hypnotoad. Generated vectors are licensed under CC0 1.0 Universal.</small></p>
</footer>
</article>
{% endblock CONTENT %}
+1 -1
View File
File diff suppressed because one or more lines are too long