From c7db03865939ed295588435e2227976a08a710fb Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Sun, 17 May 2026 18:19:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(lint):=20WCAG-=D0=BA=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D1=82=20=D0=B2=20CSS-=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D0=B5=20+=20fix=20shipments=20Drive=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lint_css.py: проверка контраста по WCAG 4.5:1 для HEX-цветов, разделение ошибок/предупреждений, проверка против всех тем - config.py: SHIPMENTS_FILE_ID обновлён на актуальный из AI АНАЛИТИКА ARRIVALS_FILE_ID сброшен в пустой (ID пока не найден) Co-Authored-By: Claude Sonnet 4.6 --- backend-py/app/config.py | 6 +- tests/lint_css.py | 283 +++++++++++++++++++++++++++------------ 2 files changed, 203 insertions(+), 86 deletions(-) diff --git a/backend-py/app/config.py b/backend-py/app/config.py index fd9357a..c5796de 100644 --- a/backend-py/app/config.py +++ b/backend-py/app/config.py @@ -55,6 +55,8 @@ def get_config() -> Config: proxy_static_list=os.getenv("PROXY_STATIC_LIST", ""), proxy_list_file=os.getenv("PROXY_LIST_FILE", ""), internal_secret=os.getenv("INTERNAL_SECRET", ""), - shipments_file_id=os.getenv("SHIPMENTS_FILE_ID", "1fER4NmEgSznvPKJWXOqLDDkTxH6wm78E"), - arrivals_file_id=os.getenv("ARRIVALS_FILE_ID", "1kgrDEIGcVMFnSdZs1Y_QHVhjqsXFQk2h"), + # ОТГРУЗКИ — актуальный ID из AI АНАЛИТИКА/_sources_config.json + shipments_file_id=os.getenv("SHIPMENTS_FILE_ID", "1KCJUXjhVR2NWEz9bD0kjTaEADsxF8gI5GMzLwJ2bw84"), + # Поступление заказов на склад СПб — заполнить в deploy/.env когда найдут файл + arrivals_file_id=os.getenv("ARRIVALS_FILE_ID", ""), ) diff --git a/tests/lint_css.py b/tests/lint_css.py index 6bb5bb0..3617808 100644 --- a/tests/lint_css.py +++ b/tests/lint_css.py @@ -1,63 +1,147 @@ """ CSS-линтер для miniapp/assets/ -Запуск: python tests/lint_css.py +Проверяет: запрещённые паттерны, читаемость цветов (контраст WCAG), + явные цвета в ключевых классах, версии кэша. +Запуск: python -X utf8 tests/lint_css.py Возвращает exit code 1 если найдены проблемы. """ import re import sys +import math from pathlib import Path +from datetime import datetime ROOT = Path(__file__).parent.parent / "miniapp" / "assets" -ISSUES = [] +ISSUES = [] # критические — блокируют коммит +WARNINGS = [] # предупреждения — не блокируют, но стоит исправить def issue(file: str, line: int, msg: str): ISSUES.append(f" ❌ {file}:{line} {msg}") +def warn(file: str, line: int, msg: str): + WARNINGS.append(f" ⚠️ {file}:{line} {msg}") -# ─── Правила ──────────────────────────────────────────────────────────────── -# 1. Запрещённые паттерны скрытия текста -FORBIDDEN_COLOR_PATTERNS = [ +# ════════════════════════════════════════════════════════════════ +# WCAG-контраст +# ════════════════════════════════════════════════════════════════ + +def _hex_to_rgb(h: str) -> tuple[int, int, int] | None: + h = h.lstrip("#") + if len(h) == 3: + h = "".join(c*2 for c in h) + if len(h) != 6: + return None + try: + return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + except ValueError: + return None + + +def _relative_luminance(r: int, g: int, b: int) -> float: + def c(x): + x /= 255 + return x / 12.92 if x <= 0.04045 else ((x + 0.055) / 1.055) ** 2.4 + return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b) + + +def contrast_ratio(hex1: str, hex2: str) -> float | None: + rgb1 = _hex_to_rgb(hex1) + rgb2 = _hex_to_rgb(hex2) + if not rgb1 or not rgb2: + return None + L1 = _relative_luminance(*rgb1) + L2 = _relative_luminance(*rgb2) + lighter, darker = max(L1, L2), min(L1, L2) + return (lighter + 0.05) / (darker + 0.05) + + +def contrast_grade(ratio: float | None) -> str: + if ratio is None: + return "?" + if ratio >= 7.0: + return "AAA" + if ratio >= 4.5: + return "AA" + if ratio >= 3.0: + return "AA-Large" + return "FAIL" + + +# Известные фоны карточек по темам (для проверки текста без background в блоке) +CARD_BACKGROUNDS = [ + "#FFFFFF", # Default light + "#EAE3CC", # Foundry + "#EDE5D0", # Boardroom + "#E9EBEF", # Atelier +] +PAGE_BACKGROUNDS = [ + "#FAFAF7", # Default light + "#14130E", # Default dark + "#EFE9D8", # Foundry + "#F2E9D6", # Boardroom + "#E9EBEF", # Atelier +] + + +# ════════════════════════════════════════════════════════════════ +# Паттерны-запреты (построчные) +# ════════════════════════════════════════════════════════════════ + +FORBIDDEN_LINE_PATTERNS = [ ( - # Только свойство color:, но НЕ -webkit-tap-highlight-color: r"(? str | None: + m = re.search(pattern, text, re.IGNORECASE) + return m.group(1) if m else None + + +def _check_color_against_known_backgrounds( + fname: str, line: int, selector: str, color_hex: str +): + """Проверяет цвет текста против всех известных фонов карточек.""" + fails = [] + for bg in CARD_BACKGROUNDS: + ratio = contrast_ratio(color_hex, bg) + if ratio is not None and ratio < 4.5: + fails.append(f"{bg}={ratio:.1f}:1") + + if len(fails) == len(CARD_BACKGROUNDS): + # Плохой контраст против ВСЕХ фонов — критично + issue(fname, line, + f"`{selector}` color:{color_hex} — низкий контраст против всех " + f"фонов карточек: {', '.join(fails)}. Текст нечитаем во всех темах") + elif fails: + warn(fname, line, + f"`{selector}` color:{color_hex} — низкий контраст в некоторых темах: " + f"{', '.join(fails)}. Проверь Foundry/Boardroom/Atelier") + + +# ════════════════════════════════════════════════════════════════ +# Проверка версий в index.html +# ════════════════════════════════════════════════════════════════ def lint_versions(): index = Path(__file__).parent.parent / "miniapp" / "index.html" if not index.exists(): return - text = index.read_text(encoding="utf-8") - versions = {} + text = index.read_text(encoding="utf-8") + versions: dict[str, str] = {} - # Собираем версии из href/src for m in re.finditer(r'(?:href|src)="assets/([^"]+)\?v=([^"]+)"', text): - fname, ver = m.group(1), m.group(2) - versions[fname] = ver + versions[m.group(1)] = m.group(2) - # Проверяем что CSS и JS версии не слишком старые (> 30 дней назад) - from datetime import datetime - today_str = datetime.now().strftime("%Y%m%d") + today = datetime.now().strftime("%Y%m%d") for fname, ver in versions.items(): if re.match(r"^\d{8}", ver): - file_date = ver[:8] - # Сравниваем как строки (работает для YYYYMMDD) - days_diff = (datetime.strptime(today_str, "%Y%m%d") - - datetime.strptime(file_date, "%Y%m%d")).days + days_diff = (datetime.strptime(today, "%Y%m%d") - + datetime.strptime(ver[:8], "%Y%m%d")).days if days_diff > 30: - ISSUES.append( - f" ⚠️ index.html: {fname} версия «{ver}» устарела " - f"на {days_diff} дней — возможно забыли обновить версию при последнем изменении" - ) + warn("index.html", 0, + f"{fname} версия «{ver}» не обновлялась {days_diff} дней — " + f"проверь, не забыли ли поднять версию после изменений") - # Форматы версии должны совпадать с шаблоном YYYYMMDDx - for fname, ver in versions.items(): if not re.match(r"^\d{8}[a-z]$", ver): - ISSUES.append( - f" ⚠️ index.html: {fname} версия «{ver}» не соответствует формату " - f"YYYYMMDDx (например 20260517j)" - ) + warn("index.html", 0, + f"{fname} версия «{ver}» не соответствует формату YYYYMMDDx") -# ─── Main ─────────────────────────────────────────────────────────────────── +# ════════════════════════════════════════════════════════════════ +# Main +# ════════════════════════════════════════════════════════════════ def main(): print("🔍 CSS-линтер miniapp/assets/\n") - css_files = list(ROOT.glob("*.css")) - if not css_files: - print(" Нет CSS-файлов для анализа.") - sys.exit(0) - - for f in sorted(css_files): + for f in sorted(ROOT.glob("*.css")): lint_file(f) lint_versions() + if WARNINGS: + print("Предупреждения (не блокируют, но стоит исправить):\n") + for w in WARNINGS: + print(w) + print() + if ISSUES: - print("Найдены проблемы:\n") + print("Критические проблемы (блокируют коммит):\n") for iss in ISSUES: print(iss) - print(f"\n🚫 Итого: {len(ISSUES)} замечание(й). Исправь перед коммитом.\n") + print(f"\n🚫 Итого: {len(ISSUES)} ошибок, {len(WARNINGS)} предупреждений. " + f"Исправь ошибки перед коммитом.\n") sys.exit(1) + elif WARNINGS: + print(f"⚠️ Итого: 0 ошибок, {len(WARNINGS)} предупреждений.\n") + sys.exit(0) else: - print("✅ Всё чисто — замечаний нет.\n") + print("✅ Всё чисто — ни ошибок, ни предупреждений.\n") sys.exit(0)