Compare commits
4 Commits
feature/cr
...
fix/dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52c5cc1af1 | ||
| c97ced52b4 | |||
|
|
deaecd8d3b | ||
| e4d4271e32 |
@@ -29,7 +29,7 @@ def dashboard(request: Request, db: Session = Depends(get_db), settings: Setting
|
||||
counts = stats_payload(db)
|
||||
counts["runs"] = db.scalar(select(func.count()).select_from(CrawlRun)) or 0
|
||||
counts["errors"] = db.scalar(select(func.count()).select_from(CrawlError)) or 0
|
||||
run_models = db.scalars(select(CrawlRun).order_by(desc(CrawlRun.started_at)).limit(10)).all()
|
||||
run_models = db.scalars(select(CrawlRun).order_by(desc(CrawlRun.started_at)).limit(5)).all()
|
||||
runs = [run_payload(run) for run in run_models]
|
||||
return _render(request, "dashboard.html", {"counts": counts, "runs": runs, "latest_run": runs[0] if runs else None})
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.admin {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #1f2937;
|
||||
background: #f6f7f9;
|
||||
font-family: Arial, sans-serif;
|
||||
@@ -21,6 +23,11 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.admin__brand-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.admin__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -34,6 +41,7 @@
|
||||
}
|
||||
|
||||
.admin__main {
|
||||
flex: 1;
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 28px auto;
|
||||
}
|
||||
@@ -52,18 +60,30 @@
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: block;
|
||||
padding: 18px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9dee7;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.metric--link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.metric--link:hover {
|
||||
border-color: #0f766e;
|
||||
}
|
||||
|
||||
.metric__label {
|
||||
display: block;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.metric__value {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
@@ -87,6 +107,14 @@
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table__row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table__row:hover {
|
||||
background: #f0fdfa;
|
||||
}
|
||||
|
||||
.table__cell,
|
||||
.table__head {
|
||||
padding: 10px 8px;
|
||||
@@ -331,12 +359,22 @@
|
||||
}
|
||||
|
||||
.stats-strip__item {
|
||||
display: block;
|
||||
padding: 14px 16px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9dee7;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stats-strip__item--link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.stats-strip__item--link:hover {
|
||||
border-color: #0f766e;
|
||||
}
|
||||
|
||||
.stats-strip__label {
|
||||
display: block;
|
||||
color: #6b7280;
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
applyColumns(columns);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupClickableRows() {
|
||||
document.querySelectorAll("[data-row-href]").forEach((row) => {
|
||||
row.addEventListener("click", (event) => {
|
||||
if (event.target.closest("a, button, input, select, label")) return;
|
||||
@@ -107,5 +110,6 @@
|
||||
}
|
||||
|
||||
setupColumns();
|
||||
setupClickableRows();
|
||||
setupProgress();
|
||||
})();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</head>
|
||||
<body class="admin">
|
||||
<header class="admin__header">
|
||||
<h1 class="admin__brand">MIEM Employees</h1>
|
||||
<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>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
{% block title %}Обзор · MIEM Employees{% endblock %}
|
||||
{% block content %}
|
||||
<section class="admin__grid">
|
||||
<div class="metric"><div class="metric__label">Всего в базе</div><div class="metric__value">{{ counts.total }}</div></div>
|
||||
<div class="metric"><div class="metric__label">Работают</div><div class="metric__value">{{ counts.active }}</div></div>
|
||||
<div class="metric"><div class="metric__label">Новые за запуск</div><div class="metric__value">{{ counts.new_in_last_run }}</div></div>
|
||||
<div class="metric"><div class="metric__label">Уволены</div><div class="metric__value">{{ counts.dismissed }}</div></div>
|
||||
<a class="metric metric--link" href="/admin/directory"><span class="metric__label">Всего в базе</span><span class="metric__value">{{ counts.total }}</span></a>
|
||||
<a class="metric metric--link" href="/admin/directory?status=active"><span class="metric__label">Работают</span><span class="metric__value">{{ counts.active }}</span></a>
|
||||
<a class="metric metric--link" href="{% if latest_run %}/admin/runs/{{ latest_run.id }}#new-employees{% else %}/admin/runs{% endif %}"><span class="metric__label">Новые за запуск</span><span class="metric__value">{{ counts.new_in_last_run }}</span></a>
|
||||
<a class="metric metric--link" href="/admin/directory?status=dismissed"><span class="metric__label">Уволены</span><span class="metric__value">{{ counts.dismissed }}</span></a>
|
||||
</section>
|
||||
<section class="stats-strip">
|
||||
<div class="stats-strip__item">
|
||||
@@ -16,10 +16,10 @@
|
||||
<span class="stats-strip__value">Сотрудников пока нет</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="stats-strip__item">
|
||||
<a class="stats-strip__item stats-strip__item--link" href="/admin/runs">
|
||||
<span class="stats-strip__label">Запуски</span>
|
||||
<span class="stats-strip__value">{{ counts.runs }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="stats-strip__item">
|
||||
<span class="stats-strip__label">Ошибки</span>
|
||||
<span class="stats-strip__value">{{ counts.errors }}</span>
|
||||
@@ -51,7 +51,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></tr></thead>
|
||||
<tbody>
|
||||
{% for run in runs %}
|
||||
<tr><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.error_count }}</td><td class="table__cell">{{ run.started_display }}</td></tr>
|
||||
<tr class="table__row" data-row-href="/admin/runs/{{ run.id }}"><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.error_count }}</td><td class="table__cell">{{ run.started_display }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</section>
|
||||
|
||||
{% for group, title in [("new", "Новые сотрудники"), ("missing_from_source", "Потеряшки"), ("dismissed", "Уволенные")] %}
|
||||
<section class="panel">
|
||||
<section class="panel"{% if group == "new" %} id="new-employees"{% endif %}>
|
||||
<h2 class="panel__title">{{ title }}</h2>
|
||||
{% set items = run.changes[group] %}
|
||||
{% if items %}
|
||||
|
||||
@@ -38,7 +38,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><th class="table__head">Уволены</th><th class="table__head">Старт</th></tr></thead>
|
||||
<tbody>
|
||||
{% for run in runs %}
|
||||
<tr><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.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" data-row-href="/admin/runs/{{ run.id }}"><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.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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
APP_VERSION = "0.4.0"
|
||||
FRONTEND_VERSION = "0.4.0"
|
||||
BACKEND_VERSION = "0.4.0"
|
||||
APP_VERSION = "0.4.2"
|
||||
FRONTEND_VERSION = "0.4.2"
|
||||
BACKEND_VERSION = "0.4.2"
|
||||
|
||||
@@ -8,6 +8,7 @@ def test_base_navigation_is_russian_and_has_no_legacy_employees_link():
|
||||
assert "Сотрудники" in template
|
||||
assert "Запуски" in template
|
||||
assert "Выйти" in template
|
||||
assert '<a class="admin__brand-link" href="/admin">MIEM Employees</a>' in template
|
||||
assert ">Employees<" not in template
|
||||
assert "/admin/employees" not in template
|
||||
|
||||
@@ -34,17 +35,43 @@ def test_admin_employees_route_redirects_to_directory():
|
||||
assert 'RedirectResponse("/admin/directory", status_code=303)' in source
|
||||
|
||||
|
||||
def test_dashboard_limits_latest_runs_to_five():
|
||||
source = Path("app/admin.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "order_by(desc(CrawlRun.started_at)).limit(5)" in source
|
||||
assert "order_by(desc(CrawlRun.started_at)).limit(10)" not in source
|
||||
|
||||
|
||||
def test_runs_template_links_to_run_detail():
|
||||
template = Path("app/templates/runs.html").read_text(encoding="utf-8")
|
||||
|
||||
assert 'href="/admin/runs/{{ run.id }}"' in template
|
||||
assert 'data-row-href="/admin/runs/{{ run.id }}"' in template
|
||||
|
||||
|
||||
def test_run_detail_template_extends_base_and_shows_change_groups():
|
||||
template = Path("app/templates/run_detail.html").read_text(encoding="utf-8")
|
||||
|
||||
assert '{% extends "base.html" %}' in template
|
||||
assert 'id="new-employees"' in template
|
||||
assert "Новые сотрудники" in template
|
||||
assert "Потеряшки" in template
|
||||
assert "Уволенные" in template
|
||||
assert "Детализация сотрудников для этого запуска недоступна" in template
|
||||
|
||||
|
||||
def test_dashboard_metric_cards_link_to_admin_targets():
|
||||
template = Path("app/templates/dashboard.html").read_text(encoding="utf-8")
|
||||
|
||||
assert 'href="/admin/directory"' in template
|
||||
assert 'href="/admin/directory?status=active"' in template
|
||||
assert '/admin/runs/{{ latest_run.id }}#new-employees' in template
|
||||
assert 'href="/admin/directory?status=dismissed"' in template
|
||||
assert 'href="/admin/runs"' in template
|
||||
|
||||
|
||||
def test_dashboard_latest_run_rows_link_to_run_detail():
|
||||
template = Path("app/templates/dashboard.html").read_text(encoding="utf-8")
|
||||
|
||||
assert 'data-row-href="/admin/runs/{{ run.id }}"' in template
|
||||
assert 'href="/admin/runs/{{ run.id }}"' in template
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_health_returns_versions():
|
||||
response = client.get("/api/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["backend_version"] == "0.4.0"
|
||||
assert response.json()["backend_version"] == "0.4.2"
|
||||
|
||||
|
||||
def test_mcp_requires_token_and_lists_tools():
|
||||
|
||||
Reference in New Issue
Block a user