fix: harden admin UX and accessibility

This commit is contained in:
2026-08-28 12:11:52 +03:00
parent dc060985a3
commit 5d3fb68c2f
11 changed files with 294 additions and 139 deletions

View File

@@ -7,18 +7,19 @@
<link rel="stylesheet" href="/static/admin.css">
</head>
<body class="admin">
<a class="admin__skip-link" href="#main-content">Перейти к содержимому</a>
<header class="admin__header">
<h1 class="admin__brand"><a class="admin__brand-link" href="/admin">MIEM Employees</a></h1>
<nav class="admin__nav">
<a class="admin__link" href="/admin">Обзор</a>
<a class="admin__link" href="/admin/directory">Сотрудники</a>
<a class="admin__link" href="/admin/runs">Запуски</a>
<a class="admin__link" href="/admin"{% if request.url.path == "/admin" %} aria-current="page"{% endif %}>Обзор</a>
<a class="admin__link" href="/admin/directory"{% if request.url.path == "/admin/directory" %} aria-current="page"{% endif %}>Сотрудники</a>
<a class="admin__link" href="/admin/runs"{% if request.url.path.startswith("/admin/runs") %} aria-current="page"{% endif %}>Запуски</a>
<form method="post" action="/admin/logout">
<button class="button button--ghost" type="submit">Выйти</button>
</form>
</nav>
</header>
<main class="admin__main">
<main class="admin__main" id="main-content">
{% block content %}{% endblock %}
</main>
<footer class="admin__footer">

View File

@@ -28,15 +28,15 @@
</section>
<section class="panel progress-panel" data-progress-panel>
<div class="progress-panel__header">
<h2 class="panel__title">Прогресс парсинга</h2>
<div class="progress-panel__actions">
<form method="post" action="/admin/crawl-now">
<button class="button" type="submit">Запустить парсинг</button>
</form>
<form method="post" action="/admin/dismissed/refresh">
<button class="button button--secondary" type="submit">Проверить уволенных</button>
</form>
</div>
<h2 class="panel__title">Прогресс парсинга</h2>
<div class="progress-panel__actions">
<form method="post" action="/admin/crawl-now">
<button class="button" type="submit">Запустить парсинг</button>
</form>
<form method="post" action="/admin/dismissed/refresh">
<button class="button button--secondary" type="submit">Проверить уволенных</button>
</form>
</div>
</div>
{% set run = counts.current_running_run or latest_run %}
<div class="progress-panel__body" data-progress-body>
@@ -46,10 +46,11 @@
<span>без изменений: <span data-progress-skipped>{{ run.skipped_count if run else 0 }}</span></span>
<span>ошибок: <span data-progress-errors>{{ run.error_count if run else 0 }}</span></span>
</div>
<div class="progress-bar" aria-label="Parsing progress">
<div class="progress-bar" role="progressbar" aria-label="Прогресс парсинга" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ run.progress_percent if run else 0 }}" aria-valuetext="{{ run.progress_percent if run else 0 }}%">
<div class="progress-bar__fill" data-progress-fill style="width: {{ run.progress_percent if run else 0 }}%"></div>
</div>
<div class="progress-panel__percent"><span data-progress-percent>{{ run.progress_percent if run else 0 }}</span>%</div>
<p class="progress-panel__error" data-progress-error role="status" hidden>Не удалось обновить прогресс. Повторите позже.</p>
</div>
</section>
<section class="panel">
@@ -58,7 +59,7 @@
<thead><tr><th class="table__head">ID</th><th class="table__head">Статус</th><th class="table__head">Обработано</th><th class="table__head">Без изменений</th><th class="table__head">Ошибки</th><th class="table__head">Старт</th></tr></thead>
<tbody>
{% for run in runs %}
<tr class="table__row" onclick="window.location.href='/admin/runs/{{ run.id }}'" onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.location.href='/admin/runs/{{ run.id }}'; }" role="link" tabindex="0"><td class="table__cell">{{ run.id }}</td><td class="table__cell">{{ run.status_display }}</td><td class="table__cell">{{ run.parsed_count }}</td><td class="table__cell">{{ run.skipped_count }}</td><td class="table__cell">{{ run.error_count }}</td><td class="table__cell">{{ run.started_display }}</td></tr>
<tr class="table__row"><td class="table__cell"><a class="admin__link" href="/admin/runs/{{ run.id }}">{{ run.id }}</a></td><td class="table__cell">{{ run.status_display }}</td><td class="table__cell">{{ run.parsed_count }}</td><td class="table__cell">{{ run.skipped_count }}</td><td class="table__cell">{{ run.error_count }}</td><td class="table__cell">{{ run.started_display }}</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -5,47 +5,64 @@
<div class="directory__header">
<div>
<h2 class="directory__title">Сотрудники</h2>
<p class="directory__summary">Найдено: {{ page.total }}</p>
{% set range_start = page.offset + 1 if page.total else 0 %}
{% set range_end = [page.offset + page.employees|length, page.total]|min %}
<p class="directory__summary" role="status">Показаны {{ range_start }}{{ range_end }} из {{ page.total }}</p>
</div>
<button class="button" type="button" data-columns-open>Колонки</button>
<button class="button" type="button" data-columns-open aria-controls="columns-dialog" aria-expanded="false">Настроить колонки</button>
</div>
<form class="directory__filters" method="get" action="/admin/directory">
<input class="directory__input" name="q" value="{{ filters.q }}" placeholder="ФИО или ссылка">
<select class="directory__input" name="status">
<fieldset class="directory__filter-group directory__filter-group--search">
<legend class="directory__filter-legend">Поиск</legend>
<label class="directory__field">ФИО или ссылка<input class="directory__input" name="q" value="{{ filters.q }}" placeholder="Например, Иванов или hse.ru"></label>
</fieldset>
<fieldset class="directory__filter-group">
<legend class="directory__filter-legend">Статус и данные</legend>
<label class="directory__field">Статус<select class="directory__input" name="status">
<option value="" {% if not filters.status %}selected{% endif %}>Все статусы</option>
<option value="active" {% if filters.status == "active" %}selected{% endif %}>Работает</option>
<option value="verification_required" {% if filters.status == "verification_required" %}selected{% endif %}>Требует проверки</option>
<option value="dismissed" {% if filters.status == "dismissed" %}selected{% endif %}>Уволен</option>
</select>
<select class="directory__input" name="has_email">
</select></label>
<label class="directory__field">Email<select class="directory__input" name="has_email">
<option value="" {% if not filters.has_email %}selected{% endif %}>Любой email</option>
<option value="true" {% if filters.has_email == "true" %}selected{% endif %}>Есть email</option>
<option value="false" {% if filters.has_email == "false" %}selected{% endif %}>Нет email</option>
</select>
<select class="directory__input" name="has_academic_degree" aria-label="Учёная степень">
</select></label>
<label class="directory__field">Учёная степень<select class="directory__input" name="has_academic_degree">
<option value="" {% if not filters.has_academic_degree %}selected{% endif %}>Любая учёная степень</option>
<option value="true" {% if filters.has_academic_degree == "true" %}selected{% endif %}>Есть учёная степень</option>
<option value="false" {% if filters.has_academic_degree == "false" %}selected{% endif %}>Нет учёной степени</option>
</select>
<input class="directory__input" type="date" name="started_from" value="{{ filters.started_from }}" aria-label="Впервые найден с">
<input class="directory__input" type="date" name="started_to" value="{{ filters.started_to }}" aria-label="Впервые найден по">
<select class="directory__input" name="sort">
</select></label>
</fieldset>
<fieldset class="directory__filter-group">
<legend class="directory__filter-legend">Период обнаружения</legend>
<label class="directory__field">От<input class="directory__input" type="date" name="started_from" value="{{ filters.started_from }}"></label>
<label class="directory__field">До<input class="directory__input" type="date" name="started_to" value="{{ filters.started_to }}"></label>
</fieldset>
<fieldset class="directory__filter-group">
<legend class="directory__filter-legend">Сортировка и страница</legend>
<label class="directory__field">Сортировать по<select class="directory__input" name="sort">
{% for value, label in [("full_name", "ФИО"), ("status", "Статус"), ("hse_start_year", "Год начала"), ("first_seen_at", "Впервые найден"), ("last_seen_at", "Последний раз найден"), ("dismissed_at", "Дата увольнения")] %}
<option value="{{ value }}" {% if filters.sort == value %}selected{% endif %}>Сортировка: {{ label }}</option>
{% endfor %}
</select>
<select class="directory__input" name="direction">
</select></label>
<label class="directory__field">Направление<select class="directory__input" name="direction">
<option value="asc" {% if filters.direction == "asc" %}selected{% endif %}>По возрастанию</option>
<option value="desc" {% if filters.direction == "desc" %}selected{% endif %}>По убыванию</option>
</select>
<select class="directory__input" name="limit" onchange="this.form.offset.value = 0; this.form.submit()">
</select></label>
<label class="directory__field">Сотрудников на странице<select class="directory__input" name="limit">
{% for value in [25, 50, 100] %}
<option value="{{ value }}" {% if filters.limit == value %}selected{% endif %}>На странице: {{ value }}</option>
{% endfor %}
</select>
</select></label>
</fieldset>
<input type="hidden" name="offset" value="{{ filters.offset }}">
<button class="button" type="submit">Применить</button>
<div class="directory__filter-actions">
<button class="button" type="submit">Применить фильтры</button>
<a class="button button--ghost" href="/admin/directory">Сбросить</a>
</div>
</form>
<div class="directory__table-wrap">
@@ -71,9 +88,9 @@
</thead>
<tbody>
{% for employee in page.employees %}
<tr class="directory-table__row" data-row-href="/admin/employees/{{ employee.id }}">
<td class="directory-table__cell" data-column="full_name">{{ employee.full_name or "Без имени" }}</td>
<td class="directory-table__cell" data-column="status"><span class="badge {% if employee.status == "dismissed" %}badge--dismissed{% endif %}">{{ employee.status_display }}</span></td>
<tr class="directory-table__row">
<td class="directory-table__cell" data-column="full_name"><a class="admin__link" href="/admin/employees/{{ employee.id }}">{{ employee.full_name or "Без имени" }}</a></td>
<td class="directory-table__cell" data-column="status"><span class="badge {% if employee.status == "dismissed" %}badge--dismissed{% elif employee.status == "verification_required" %}badge--verification{% endif %}">{{ employee.status_display }}</span></td>
<td class="directory-table__cell" data-column="positions">{{ employee.positions_text }}</td>
<td class="directory-table__cell" data-column="hse_start_year">{{ employee.hse_start_year or "" }}</td>
<td class="directory-table__cell" data-column="email">{{ employee.email_text }}</td>
@@ -86,10 +103,18 @@
<td class="directory-table__cell" data-column="first_seen_at">{{ employee.first_seen_display }}</td>
<td class="directory-table__cell" data-column="last_seen_at">{{ employee.last_seen_display }}</td>
<td class="directory-table__cell" data-column="dismissed_at">{{ employee.dismissed_display }}</td>
<td class="directory-table__cell" data-column="profile"><a class="admin__link" href="{{ employee.canonical_url }}">Открыть</a></td>
<td class="directory-table__cell" data-column="profile"><a class="admin__link" href="{{ employee.canonical_url }}" target="_blank" rel="noopener">Внешний профиль</a></td>
</tr>
{% else %}
<tr><td class="directory-table__empty" colspan="15">По этим фильтрам сотрудники не найдены.</td></tr>
<tr><td class="directory-table__empty" colspan="15">
<strong>Сотрудники не найдены</strong>
{% if filters.q or filters.status or filters.has_email or filters.has_academic_degree or filters.started_from or filters.started_to %}
<span>Попробуйте изменить запрос или сбросить фильтры.</span>
<a class="admin__link" href="/admin/directory">Сбросить фильтры</a>
{% else %}
<span>Запустите парсинг, чтобы загрузить сотрудников.</span>
{% endif %}
</td></tr>
{% endfor %}
</tbody>
</table>
@@ -108,11 +133,10 @@
</div>
</section>
<div class="columns-modal" data-columns-modal hidden>
<div class="columns-modal__backdrop" data-columns-close></div>
<section class="columns-modal__panel" aria-label="Настройка колонок">
<dialog class="columns-modal" id="columns-dialog" data-columns-modal>
<section class="columns-modal__panel" aria-labelledby="columns-dialog-title">
<div class="columns-modal__header">
<h3 class="columns-modal__title">Отображаемые колонки</h3>
<h3 class="columns-modal__title" id="columns-dialog-title">Отображаемые колонки</h3>
<button class="button button--ghost" type="button" data-columns-close>Закрыть</button>
</div>
<div class="columns-modal__grid">
@@ -121,7 +145,7 @@
{% endfor %}
</div>
</section>
</div>
</dialog>
{% endblock %}
{% block scripts %}
<script src="/static/admin.js"></script>

View File

@@ -17,10 +17,11 @@
<span>без изменений: <span data-progress-skipped>{{ run.skipped_count }}</span></span>
<span>ошибок: <span data-progress-errors>{{ run.error_count }}</span></span>
</div>
<div class="progress-bar" aria-label="Parsing progress">
<div class="progress-bar" role="progressbar" aria-label="Прогресс парсинга" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ percent }}" aria-valuetext="{{ percent }}%">
<div class="progress-bar__fill" data-progress-fill style="width: {{ percent }}%"></div>
</div>
<div class="progress-panel__percent"><span data-progress-percent>{{ percent }}</span>%</div>
<p class="progress-panel__error" data-progress-error role="status" hidden>Не удалось обновить прогресс. Повторите позже.</p>
</div>
{% else %}
<div class="progress-panel" data-progress-panel>
@@ -30,17 +31,18 @@
<span>без изменений: <span data-progress-skipped>0</span></span>
<span>ошибок: <span data-progress-errors>0</span></span>
</div>
<div class="progress-bar" aria-label="Parsing progress">
<div class="progress-bar" role="progressbar" aria-label="Прогресс парсинга" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-valuetext="0%">
<div class="progress-bar__fill" data-progress-fill style="width: 0%"></div>
</div>
<div class="progress-panel__percent"><span data-progress-percent>0</span>%</div>
<p class="progress-panel__error" data-progress-error role="status" hidden>Не удалось обновить прогресс. Повторите позже.</p>
</div>
{% endif %}
<table class="table">
<thead><tr><th class="table__head">ID</th><th class="table__head">Статус</th><th class="table__head">Найдено</th><th class="table__head">Обработано</th><th class="table__head">Без изменений</th><th class="table__head">Новые</th><th class="table__head">Ошибки</th><th class="table__head">Уволены</th><th class="table__head">Старт</th></tr></thead>
<tbody>
{% for run in runs %}
<tr class="table__row" onclick="window.location.href='/admin/runs/{{ run.id }}'" onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.location.href='/admin/runs/{{ run.id }}'; }" role="link" tabindex="0"><td class="table__cell">{{ run.id }}</td><td class="table__cell">{{ run.status_display }}</td><td class="table__cell">{{ run.found_count }}</td><td class="table__cell">{{ run.parsed_count }}</td><td class="table__cell">{{ run.skipped_count }}</td><td class="table__cell">{{ run.new_count }}</td><td class="table__cell">{{ run.error_count }}</td><td class="table__cell">{{ run.dismissed_count }}</td><td class="table__cell">{{ run.started_display }}</td></tr>
<tr class="table__row"><td class="table__cell"><a class="admin__link" href="/admin/runs/{{ run.id }}">{{ run.id }}</a></td><td class="table__cell">{{ run.status_display }}</td><td class="table__cell">{{ run.found_count }}</td><td class="table__cell">{{ run.parsed_count }}</td><td class="table__cell">{{ run.skipped_count }}</td><td class="table__cell">{{ run.new_count }}</td><td class="table__cell">{{ run.error_count }}</td><td class="table__cell">{{ run.dismissed_count }}</td><td class="table__cell">{{ run.started_display }}</td></tr>
{% endfor %}
</tbody>
</table>