Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f16e1a42 | ||
|
|
dc1897cc62 |
+14
-14
@@ -6,25 +6,25 @@ class HalftoneGenerateForm(forms.Form):
|
||||
Форма валидации входных параметров для генерации гипнотического SVG.
|
||||
"""
|
||||
SHAPE_CHOICES = [
|
||||
("circle", "Круг"),
|
||||
("ring", "Кольцо"),
|
||||
("triangle", "Треугольник"),
|
||||
("square", "Квадрат"),
|
||||
("diamond", "Ромб"),
|
||||
("hexagon", "Шестиугольник"),
|
||||
("star", "Звезда"),
|
||||
("cross", "Крестик"),
|
||||
("line", "Линия"),
|
||||
("snowflake", "Снежинка"),
|
||||
("binary", "Матрица (0/1)"),
|
||||
("heart", "Сердечко"),
|
||||
("circle", "Circle"),
|
||||
("ring", "Ring"),
|
||||
("triangle", "Triangle"),
|
||||
("square", "Square"),
|
||||
("diamond", "Diamond"),
|
||||
("hexagon", "Hexagon"),
|
||||
("star", "Star"),
|
||||
("cross", "Cross"),
|
||||
("line", "Line"),
|
||||
("snowflake", "Snowflake"),
|
||||
("binary", "Binary (0/1)"),
|
||||
("heart", "Heart"),
|
||||
]
|
||||
|
||||
image = forms.ImageField(
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Покорми Гипножабу изображением!",
|
||||
"invalid_image": "Гипножаба не может переварить этот файл. Нужна картинка (PNG, JPG, WEBP).",
|
||||
"required": "Feed the Hypnotoad with an image!",
|
||||
"invalid_image": "Hypnotoad cannot digest this file. Image required (PNG, JPG, WEBP).",
|
||||
},
|
||||
)
|
||||
shape = forms.ChoiceField(
|
||||
|
||||
@@ -596,13 +596,13 @@ class TbBlogPost(models.Model):
|
||||
def blog_nav_title(self) -> str:
|
||||
"""Детерминированный выбор названия раздела блога для навигации на основе ID статьи."""
|
||||
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:
|
||||
return nav_titles[0]
|
||||
|
||||
@@ -47,7 +47,7 @@ HYPNO_GENITIVES = [
|
||||
HYPNO_POSTFIXES = [
|
||||
"#42", "v3.14", "(Ultra)", "(Phase 7)", "(Alpha)", "(Omega)", "(Protocol 09)", "(Revision 14)",
|
||||
"(Extract)", "(Layer 9)", "(Ward 106)", "(Hangar 18)", "(Unfinished Opus)", "(Secret Project)",
|
||||
"[Classified]", "(DOOP-66)", "(Sub-Level 4)", "(MK-Ultra)", "С90"
|
||||
"[Classified]", "(DOOP-66)", "(Sub-Level 4)", "(MK-Ultra)", "C90"
|
||||
]
|
||||
|
||||
|
||||
|
||||
+65
-65
@@ -232,7 +232,7 @@ class HalftoneViewTests(BaseMediaTestCase):
|
||||
self.assertContains(response, 'X-CSRFToken')
|
||||
self.assertContains(response, "handleFileChange")
|
||||
self.assertContains(response, "imagePreviewUrl")
|
||||
self.assertContains(response, "Гипножаба облизывается на:")
|
||||
self.assertContains(response, "Hypnotoad salivates over:")
|
||||
# Проверяем, что csrf_token не пустой в hx-headers
|
||||
content = response.content.decode("utf-8")
|
||||
self.assertNotIn('hx-headers=\'{"X-CSRFToken": ""}\'', content)
|
||||
@@ -291,7 +291,7 @@ class HalftoneViewTests(BaseMediaTestCase):
|
||||
def test_generate_post_no_image(self):
|
||||
response = self.client.post(reverse("hypn0_site:generate"), data={"shape": "circle"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Покорми Гипножабу")
|
||||
self.assertContains(response, "Feed the Hypnotoad")
|
||||
|
||||
def test_generate_post_success(self):
|
||||
buf = io.BytesIO()
|
||||
@@ -366,8 +366,8 @@ class PublishViewTests(BaseMediaTestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Мозговые слизняки протестуют!")
|
||||
self.assertContains(response, "Подчиниться и опубликовать")
|
||||
self.assertContains(response, "Brain Slugs are rioting!")
|
||||
self.assertContains(response, "Submit and Publish")
|
||||
self.assertEqual(TbHypn0Item.objects.count(), 0)
|
||||
|
||||
def test_publish_with_cookie_success(self):
|
||||
@@ -389,8 +389,8 @@ class PublishViewTests(BaseMediaTestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Уже в галерее транса")
|
||||
self.assertContains(response, "идет прорастание психо-паутины...")
|
||||
self.assertContains(response, "In the Trance Gallery")
|
||||
self.assertContains(response, "psychic neural web expanding…")
|
||||
|
||||
# Проверяем запись в БД
|
||||
self.assertEqual(TbHypn0Item.objects.count(), 1)
|
||||
@@ -419,7 +419,7 @@ class PublishViewTests(BaseMediaTestCase):
|
||||
data={"svg_content": ""},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Нет данных SVG для публикации")
|
||||
self.assertContains(response, "No SVG data for publication")
|
||||
self.assertEqual(TbHypn0Item.objects.count(), 0)
|
||||
|
||||
|
||||
@@ -522,11 +522,11 @@ class GalleryDetailAndDownloadTests(BaseMediaTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Астральный Транс Сознания #42")
|
||||
self.assertContains(response, f"#{self.item.s_hash_id}")
|
||||
self.assertContains(response, "Векторная анатомия")
|
||||
self.assertContains(response, "Синтез психо-сетки")
|
||||
self.assertContains(response, "Астральный паспорт")
|
||||
self.assertContains(response, "Копировать SVG-код")
|
||||
self.assertContains(response, "Скачать .svg")
|
||||
self.assertContains(response, "Vector Anatomy")
|
||||
self.assertContains(response, "Psych-Grid Synthesis")
|
||||
self.assertContains(response, "Astral Passport")
|
||||
self.assertContains(response, "Copy SVG code")
|
||||
self.assertContains(response, "Download .svg")
|
||||
|
||||
# Проверка инкремента просмотров
|
||||
self.item.refresh_from_db()
|
||||
@@ -557,7 +557,7 @@ class GalleryDetailAndDownloadTests(BaseMediaTestCase):
|
||||
client = Client()
|
||||
response = client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Подчинитесь воле Гипножабы!")
|
||||
self.assertContains(response, "Surrender to Hypnotoad!")
|
||||
|
||||
|
||||
class GalleryRandomAndNavigationTests(BaseMediaTestCase):
|
||||
@@ -699,8 +699,8 @@ class UnconsciousMatrixTests(BaseMediaTestCase):
|
||||
item = self.items[0]
|
||||
response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": item.s_hash_id}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Матрица бессознательного")
|
||||
self.assertContains(response, "5 узлов")
|
||||
self.assertContains(response, "Matrix of the Unconscious")
|
||||
self.assertContains(response, "5 nodes")
|
||||
self.assertContains(response, f"#{item.s_hash_id}")
|
||||
self.assertContains(response, "btn-nav-prev")
|
||||
self.assertContains(response, "btn-nav-next")
|
||||
@@ -782,7 +782,7 @@ class GalleryFreshFloorTests(BaseMediaTestCase):
|
||||
|
||||
response = self.client.get(reverse("hypn0_site:index"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Плеск бессознательного")
|
||||
self.assertContains(response, "Incubator of the Unconscious")
|
||||
self.assertContains(response, "FRESH STREAM")
|
||||
self.assertContains(response, f"gallery-card-{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")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Галерея транса")
|
||||
self.assertContains(response, "Trance Gallery")
|
||||
self.assertContains(response, "ARCHIVE")
|
||||
self.assertContains(response, "Все волны")
|
||||
self.assertContains(response, "Инкубатор")
|
||||
self.assertContains(response, "Одобрено")
|
||||
self.assertContains(response, "Золотой фонд")
|
||||
self.assertContains(response, "All waves")
|
||||
self.assertContains(response, "Incubator")
|
||||
self.assertContains(response, "Approved")
|
||||
self.assertContains(response, "Hall of Fame")
|
||||
# Все 3 картины присутствуют в выдаче
|
||||
self.assertContains(response, f"gallery-card-{self.item_fresh.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"})
|
||||
self.assertEqual(resp_p1.status_code, 200)
|
||||
self.assertEqual(len(resp_p1.context["page_obj"]), 16)
|
||||
self.assertContains(resp_p1, "Фаза 1 из 2")
|
||||
self.assertContains(resp_p1, "<title>Галерея транса — Каталог анимированных картин | HypnoSVG</title>")
|
||||
self.assertContains(resp_p1, "Phase 1 of 2")
|
||||
self.assertContains(resp_p1, "<title>Trance Gallery — Animated SVG Catalog | HypnoSVG</title>")
|
||||
# Проверяем сохранение query-параметров в ссылках пагинатора
|
||||
self.assertContains(resp_p1, "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(len(resp_p2.context["page_obj"]), 2)
|
||||
self.assertContains(resp_p2, "Фаза 2 из 2")
|
||||
self.assertContains(resp_p2, "<title>Галерея транса — Каталог анимированных картин (Фаза 2) | HypnoSVG</title>")
|
||||
self.assertContains(resp_p2, "Phase 2 of 2")
|
||||
self.assertContains(resp_p2, "<title>Trance Gallery — Animated SVG Catalog (Phase 2) | HypnoSVG</title>")
|
||||
self.assertContains(resp_p2, "floor=all&sort=likes&page=1")
|
||||
|
||||
# Проверяем Title при фильтрации по этажу
|
||||
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):
|
||||
@@ -1311,12 +1311,12 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
# Проверяем, что отображается один из динамических заголовков секции
|
||||
content = response.content.decode("utf-8")
|
||||
possible_headings = (
|
||||
"Сигналы из параллельного континуума",
|
||||
"Психо-артефакты ноосферы",
|
||||
"Заметки астрального консилиума",
|
||||
"Рефлекторные отклики мозговых слизней",
|
||||
"Публикации из глубин подсознания",
|
||||
"Материалы хроник гипноза",
|
||||
"Signals from Parallel Continuum",
|
||||
"Noosphere Psycho-Artifacts",
|
||||
"Astral Council Field Notes",
|
||||
"Reflexive Responses of Brain Slugs",
|
||||
"Dispatches from Subconscious Depths",
|
||||
"Materials from Hypno Chronicles",
|
||||
)
|
||||
self.assertTrue(any(heading in content for heading in possible_headings))
|
||||
self.assertContains(response, "Хроники гипно-транса: Урок 1")
|
||||
@@ -1345,7 +1345,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
# Проверка 1-й страницы ленты блога
|
||||
response = self.client.get("/blog")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Хроники гипноза")
|
||||
self.assertContains(response, "Hypno Chronicles")
|
||||
self.assertContains(response, "Тестовая депеша 11")
|
||||
self.assertNotContains(response, "Черновик ноосферы")
|
||||
self.assertContains(response, "page=2")
|
||||
@@ -1388,7 +1388,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
resp_parent = self.client.get(f"/blog/{series_parent.slug}")
|
||||
self.assertEqual(resp_parent.status_code, 200)
|
||||
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, "Глава 2: Фазовый сдвиг матрицы")
|
||||
|
||||
@@ -1396,31 +1396,31 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
resp_ch1 = self.client.get(f"/blog/{ch1.slug}")
|
||||
self.assertEqual(resp_ch1.status_code, 200)
|
||||
self.assertContains(resp_ch1, "Глава 1: Первичная модуляция")
|
||||
self.assertContains(resp_ch1, "Материал входит в серию:")
|
||||
self.assertContains(resp_ch1, "Part of the series:")
|
||||
self.assertContains(resp_ch1, self.item.s_title)
|
||||
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, "Download SVG")
|
||||
self.assertContains(resp_ch1, "Weight:")
|
||||
self.assertContains(resp_ch1, "Next chapter in series")
|
||||
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, "Глава 1 из 2")
|
||||
self.assertContains(resp_ch1, "Chapter 1 of 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, '"name": "Home"')
|
||||
self.assertContains(resp_ch1, '"name": "Hypno Chronicles"')
|
||||
self.assertContains(resp_ch1, series_parent.s_title)
|
||||
|
||||
# 3. Проверка второй главы (ссылка назад на главу 1)
|
||||
resp_ch2 = self.client.get(f"/blog/{ch2.slug}")
|
||||
self.assertEqual(resp_ch2.status_code, 200)
|
||||
self.assertContains(resp_ch2, "Предыдущая глава цикла")
|
||||
self.assertContains(resp_ch2, "Previous chapter in series")
|
||||
self.assertContains(resp_ch2, "Глава 1: Первичная модуляция")
|
||||
|
||||
# 4. Проверка доступа по числовому ID
|
||||
@@ -1437,10 +1437,10 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
"""Проверка отдачи кастомной страницы 404 в режиме DEBUG=False."""
|
||||
response = self.client.get("/totally-non-existent-hypno-route-404")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertContains(response, "Гипноузел не обнаружен", status_code=404)
|
||||
self.assertContains(response, "СБОЙ МАТРИЦЫ // КОД? 404", status_code=404)
|
||||
self.assertContains(response, "Hypno-Node Not Found", status_code=404)
|
||||
self.assertContains(response, "MATRIX ANOMALY // CODE 404", status_code=404)
|
||||
self.assertContains(response, "return-btn", status_code=404)
|
||||
self.assertContains(response, "Стираем остатки несущес­твующей страницы", status_code=404)
|
||||
self.assertContains(response, "Erasing traces of the phantom page", status_code=404)
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test_debug_error_preview_routing(self):
|
||||
@@ -1448,23 +1448,23 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
# 1. Индексный список страниц ошибок
|
||||
resp_list = self.client.get("/_error/")
|
||||
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, "готов к просмотру")
|
||||
self.assertContains(resp_list, "ready to preview")
|
||||
|
||||
# 2. Проверка каждого кода ошибки
|
||||
expected_checks = [
|
||||
("400", 400, "Сбой ментального протокола"),
|
||||
("401", 401, "Требуется ментальная идентификация"),
|
||||
("403", 403, "Отказано бюрократом 24-го уровня"),
|
||||
("404", 404, "Гипноузел не обнаружен"),
|
||||
("413", 413, "Перегрузка сенсорных каналов"),
|
||||
("429", 429, "Слишком много мыслей в секунду"),
|
||||
("500", 500, "Критический перегрев неокортекса"),
|
||||
("502", 502, "Контейнер потерял сознание"),
|
||||
("503", 503, "Плановый сеанс гипнотерапии"),
|
||||
("504", 504, "Разрыв астральной связи"),
|
||||
("under_reconstruction", 200, "Отдел мозга на реконструкции"),
|
||||
("400", 400, "Perceptual Protocol Mismatch"),
|
||||
("401", 401, "Subconscious Identity Required"),
|
||||
("403", 403, "Cognitive Lockdown Active"),
|
||||
("404", 404, "Hypno-Node Not Found"),
|
||||
("413", 413, "Sensory Channel Saturation"),
|
||||
("429", 429, "Too Many Thoughts per Second"),
|
||||
("500", 500, "Introspection Neural Lock"),
|
||||
("502", 502, "Uplink to Hippocampus Lost"),
|
||||
("503", 503, "Scheduled Hypnotherapy in Progress"),
|
||||
("504", 504, "Astral Synapse Timed Out"),
|
||||
("under_reconstruction", 200, "Brain Sector Under Reconstruction"),
|
||||
]
|
||||
|
||||
for code, expected_status, text_fragment in expected_checks:
|
||||
@@ -1478,12 +1478,12 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
# 3. Предпросмотр с суффиксом .html
|
||||
resp_404_html = self.client.get("/_error/404.html")
|
||||
self.assertEqual(resp_404_html.status_code, 404)
|
||||
self.assertContains(resp_404_html, "Гипноузел не обнаружен", status_code=404)
|
||||
self.assertContains(resp_404_html, "Hypno-Node Not Found", 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)
|
||||
self.assertContains(resp_non_existent, "Template for error 'non-existent-code-999' not found", status_code=404)
|
||||
|
||||
def test_custom_django_error_handlers(self):
|
||||
"""Проверка прямого вызова стандартных Django-обработчиков ошибок."""
|
||||
@@ -1495,22 +1495,22 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
req400 = factory.get("/bad-request")
|
||||
resp400 = error_400(req400)
|
||||
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")
|
||||
resp403 = error_403(req403)
|
||||
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")
|
||||
resp404 = error_404(req404)
|
||||
self.assertEqual(resp404.status_code, 404)
|
||||
self.assertIn("Гипноузел не обнаружен", resp404.content.decode("utf-8"))
|
||||
self.assertIn("Hypno-Node Not Found", 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"))
|
||||
self.assertIn("Introspection Neural Lock", resp500.content.decode("utf-8"))
|
||||
|
||||
def test_s_title_plain_property(self):
|
||||
"""Проверка очистки HTML-тегов и мнемоник в s_title_plain для моделей TbHypn0Item и TbBlogPost."""
|
||||
|
||||
+10
-10
@@ -196,14 +196,14 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
|
||||
break
|
||||
except TemplateDoesNotExist:
|
||||
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>")
|
||||
|
||||
html = f"""<!doctype html>
|
||||
<html lang="ru">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Dev: Предпросмотр страниц ошибок // HypnoSVG</title>
|
||||
<title>Dev: Error Pages Preview // 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; }}
|
||||
@@ -215,8 +215,8 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>Центр отладки страниц ошибок (DEBUG=True)</h2>
|
||||
<p>Выберите код для инспекции верстки и интерактивных скриптов перекалибровки:</p>
|
||||
<h2>Error Pages Debug Center (DEBUG=True)</h2>
|
||||
<p>Select an error code to inspect layout and recalibration scripts:</p>
|
||||
<ul>
|
||||
{''.join(links)}
|
||||
</ul>
|
||||
@@ -244,7 +244,7 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
|
||||
|
||||
if not found_template:
|
||||
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,
|
||||
content_type="text/html; charset=utf-8",
|
||||
)
|
||||
@@ -281,7 +281,7 @@ def generate(request: HttpRequest) -> HttpResponse:
|
||||
form = HalftoneGenerateForm(request.POST, request.FILES)
|
||||
|
||||
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})
|
||||
|
||||
image_file = form.cleaned_data["image"]
|
||||
@@ -313,7 +313,7 @@ def generate(request: HttpRequest) -> HttpResponse:
|
||||
return render(
|
||||
request,
|
||||
"block/preview.html",
|
||||
{"error": f"Сбой в матрице гипноза: {str(e)}"},
|
||||
{"error": f"Glitch in hypno matrix: {str(e)}"},
|
||||
)
|
||||
|
||||
return render(
|
||||
@@ -338,7 +338,7 @@ def publish(request: HttpRequest) -> HttpResponse:
|
||||
return render(
|
||||
request,
|
||||
"block/publish_status.html",
|
||||
{"error": "Нет данных SVG для публикации. Попробуйте сгенерировать заново."},
|
||||
{"error": "No SVG data for publication. Please try generating again."},
|
||||
)
|
||||
|
||||
visitor_uuid = request.COOKIES.get("hypn0_vid")
|
||||
@@ -411,7 +411,7 @@ def publish(request: HttpRequest) -> HttpResponse:
|
||||
return render(
|
||||
request,
|
||||
"block/publish_status.html",
|
||||
{"error": f"Сбой фиксации в трансе: {str(e)}"},
|
||||
{"error": f"Failed to record in trance: {str(e)}"},
|
||||
)
|
||||
|
||||
return render(
|
||||
|
||||
@@ -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">
|
||||
<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="#" 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>{# /ПРАВОЕ МЕНЮ #}
|
||||
</div>
|
||||
</header>
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
{% 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_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 %}
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"@type": "Article",
|
||||
"headline": "{{ post.s_title|striptags|escapejs }}",
|
||||
"description": "{{ post.s_teaser|striptags|escapejs }}",
|
||||
"datePublished": "{{ post.d_published_at|date:'r' }}",
|
||||
"dateModified": "{{ post.d_updated_at|date:'r' }}",
|
||||
"datePublished": "{{ post.d_published_at|date:"r" }}",
|
||||
"dateModified": "{{ post.d_updated_at|date:"r" }}",
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": "{{ request.scheme }}://{{ request.get_host }}{{ post.get_absolute_url }}"
|
||||
@@ -116,27 +116,21 @@
|
||||
</div>
|
||||
{% 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">
|
||||
|
||||
<!-- 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">
|
||||
<div class="order-2 md:order-1 md:col-span-1 w-full pb-8">
|
||||
<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 %}{# НЕТ ТИЗЕР-КАРТИНКИ И ПОКАЗЫВАЕМ ЗАГЛУШКУ #}
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
{{ post.s_title|safe }}
|
||||
</h1>
|
||||
@@ -161,12 +155,11 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 2. Нижний ряд слева (1/4): Связанный SVG + Музейная экспликация (выровнены по вертикали с первой строкой контента) -->
|
||||
<div class="md:col-span-1 space-y-4">
|
||||
{% if post.k_item %}
|
||||
<!-- 2. Нижний ряд слева (1/4): Связанный SVG + Музейная экспликация под ним -->
|
||||
<div class="order-2 md:order-1 md:col-span-1 space-y-4 pt-6 md:pt-0">
|
||||
<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">
|
||||
<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">
|
||||
@@ -265,11 +258,10 @@
|
||||
</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="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">
|
||||
{{ post.s_content|safe }}
|
||||
</div>
|
||||
@@ -277,6 +269,164 @@
|
||||
|
||||
</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 %}
|
||||
<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="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>
|
||||
<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>
|
||||
<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">
|
||||
|
||||
+150
-64
@@ -1,69 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Security & Privacy Policy — HypnoSVG</title>
|
||||
</head>
|
||||
<body>
|
||||
TEMPORARY DRAFT
|
||||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
|
||||
<h1>Security & 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 don’t know who you are… and we don’t want to know. The Hypn0.xyz (HypnoSVG) platform operates in strict compliance with Inter­galactic DOOP Protocols and GDPR directives, adhering to the Zero Personally Identifiable Information (Zero-PII) doctrine. No registration, logins, passwords, emails, SMS, or phone numbers. The database contains zero user or account tables. Synthesized SVG files contain purely mathematical coordinates of vector primitives and inline CSS animations. No source image metadata is embedded (EXIF tags, geolocation, and hardware device profiles are purged during raster­ization).</p>
|
||||
<h2>Prologue: The Anatomy of Privacy in the Age of Mental Resonance</h2>
|
||||
<p><em>We are the Hypnotoad, the Brain Slugs, and the Receptors of Planetary Mind-Numbing. And we don’t want to know who you are. Don’t even try to persuade us.</em></p>
|
||||
<p>The Hypn0 platform is engineered from the ground up on the <strong>Zero-PII (Zero Personally Identifiable Information)</strong> doctrine. We do not require registration, logins, passwords, email addresses, or phone numbers. In the project database, user and account tables simply do not exist. Generated SVG files contain only mathematical coordinates of geometric primitives and inline CSS keyframes. Zero metadata from source images is retained (EXIF tags, GPS coordinates, device specs, and even source file names are completely stripped during raster­ization).</p>
|
||||
<hr/>
|
||||
<h2>Section I. No-Consent Mode (Default / No-Tracking)</h2>
|
||||
<p>If you have not clicked the “<strong>Submit to Hypnotoad!</strong>” button—you are completely invisible in the astral realm. This means:</p>
|
||||
<ol>
|
||||
<li>Hypn0 (HypnoSVG) does not 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 the template level and physically never loaded onto the client device.</li>
|
||||
<li><strong>Server logs</strong>—the only baseline telemetry captured by infrast­ructure (IP address, User-Agent, requested URL, and timestamp) — are utilized strictly for DDoS defense and uptime monitoring, retained for a minimal duration with regular rotation faster than memories fade after a hypnosis session.</li>
|
||||
</ol>
|
||||
<hr/>
|
||||
<h2>Section II. Restrictions for Free Humanoids Without a Brain Slug</h2>
|
||||
<p>Humanoids who decline tracking or withhold explicit consent can freely use the local vector generator and browse the gallery. However, they <strong>cannot access pseudo-social platform features</strong>:</p>
|
||||
<ul>
|
||||
<li>Publishing original works to the public “<a href="/gallery">Trance Gallery</a>”;</li>
|
||||
<li>Voting for artworks (transmitting psychic resonance / likes);</li>
|
||||
<li>Submitting complaints or flagging spam and non-transcendent content.</li>
|
||||
</ul>
|
||||
<h3>Reason for Restrictions: Anti-Abuse & Bot Defense</h3>
|
||||
<p>To operate the gravit­ational Brain Slug sorting algorithm (curated ranking) and fairly distribute artworks across tiers (“Incubator”, “Approved by Brain Slug”, “Deep Trance Hall of Fame”), the platform requires a unique anonymous client anchor.</p>
|
||||
<p>Because a humanoid who has not “surrendered to the Hypnotoad” carries no device anchor, the Lord of Mental Intros­pections (the server) cannot recognize duplicate votes. This would lead to instant scripted vote tampering, spam surges, and the destabi­lization of the entire gallery ecosystem.</p>
|
||||
<hr/>
|
||||
<h2>Section III. Consent Mechanism via “Submit to Hypnotoad!”</h2>
|
||||
<ol>
|
||||
<li>The browser generates a cryptog­raphic pseudo-random identifier via <code>crypto.randomUUID()</code> (UUID v4).</li>
|
||||
<li>The value is stored in a cookie and/or browser <code>localStorage</code> under the key <code>hypn0_vid</code>.</li>
|
||||
<li>The lifespan is configured for two Earth years, with transmission flags set to safe mode (<code>path=/; SameSite=Lax</code>).</li>
|
||||
<li>The <code>hypn0_vid</code> token is a purely random string and cannot be linked to your real identity, physical vessel, or cerebral Theta/Alpha brainwaves.</li>
|
||||
<li>The identifier is used by the backend exclusively for rate limiting and enforcing the “one vote per artwork per browser” rule.</li>
|
||||
</ol>
|
||||
<hr/>
|
||||
<h2>Section IV. File Processing Security during SVG Generation</h2>
|
||||
<ul>
|
||||
<li><strong>Input Data Sanitization (Source Images):</strong> All inbound images pass through the Pillow mental shredder, decoded exclusively in RAM, and immediately cleansed of mundane metadata. No EXIF tags, geolocation, camera models, or temporary disk artifacts remain on server storage.</li>
|
||||
<li><strong>Output Vector Security:</strong> The server stores only vector artworks synthesized by the native Hypn0 (HypnoSVG) core. Output SVGs are generated strictly program­matically and contain no <code><script></code> tags, external <code>xlink:href</code> links, or event handlers like <code>onload</code> or <code>onclick</code>. This eliminates Stored XSS risks when viewing other humanoids’ works: foreign halftones are completely harmless to your optic nerves (aside from the risk of spontaneous hypnotic trance).</li>
|
||||
<li><strong>Resource Limits (DoS Prevention):</strong> Strict thresholds are? enforced on maximum upload size, grid density (<code>cols</code>), and element radii, preventing memory exhaustion and overheating of New New York processors.</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<h2>Epilogue: Matrix Anomalies & Vulner­ability Disclosure</h2>
|
||||
<p>If you discover a security flaw capable of bypassing the will of the Hypnotoad or injecting alien code into the consciousness of Hypn0 — please approach disclosure with utmost inter­galactic respon­sibility:</p>
|
||||
<ul>
|
||||
<li>Do not publish vulner­ability details publicly, preventing space pirates and rogue bots from exploiting them before a patch is deployed.</li>
|
||||
<li>Send a detailed report to the project maintainer via the <a href="https://github.com/erjemin/2026-hypn0-site/issues" target="_blank">repository issue tracker</a> marked as security, or directly to the <a href="#" id="c-l" onclick="this.href='ma'+atob('aWx0bzplcmplb'+'WluQGdtYWlsLmNvbQ==');">admin­istrator’s email</a>.</li>
|
||||
<li>Describe attack vectors, step-by-step reproduction (PoC), and potential system impact.</li>
|
||||
<li>We will promptly acknowledge receipt and neutralize the anomaly. Please refrain from public disclosure until the vulner­ability is resolved.</li>
|
||||
</ul>
|
||||
{% 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 %}
|
||||
|
||||
<hr/>
|
||||
<hr/>
|
||||
<hr/>
|
||||
<hr/>
|
||||
{% block OG_TITLE %}About HypnoSVG — Procedural Animated Vector Halftone Engine{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% block OG_DESCRIPTION %}Deconstruct raster images into pulsating CSS-animated SVG matrices. Production-ready, ultra-lightweight, open-source, and zero JS execution overhead.{% endblock %}
|
||||
|
||||
{% 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 →</a> |
|
||||
<a href="----">Explore Gallery →</a> |
|
||||
<a href="{% url 'hypn0_site:blog_feed' %}">Read Blog →</a>
|
||||
</p>
|
||||
<p><small>All glory to the Hypnotoad. Generated vectors are licensed under CC0 1.0 Universal.</small></p>
|
||||
</footer>
|
||||
|
||||
</article>
|
||||
{% endblock CONTENT %}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user