< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Studio2_Views_JobLogs__OrderSearchTab
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/JobLogs/_OrderSearchTab.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 47
Coverable lines: 47
Total lines: 276
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 34
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExecuteAsync()0%1980440%
ToMsk()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/JobLogs/_OrderSearchTab.cshtml

#LineLine coverage
 1@model OzonOrdersWeb.Areas.Studio2.ViewModels.JobLogs.JobLogsOrderSearchTabViewModel
 2@{
 03    var s = Model.Snapshot;
 04    static DateTime ToMsk(DateTime utc) => utc.AddHours(3);
 5
 6    const int timelineSlots = 12;
 7    const int slotMinutes = 5;
 08    var timelineNow = DateTime.UtcNow;
 09    var timelineStart = timelineNow.AddMinutes(-timelineSlots * slotMinutes);
 010    var timelineBuckets = new int[timelineSlots];
 011    foreach (var ev in s.IndexEvents.Where(e => e.TimestampUtc >= timelineStart))
 12    {
 013        var minutesAgo = (timelineNow - ev.TimestampUtc).TotalMinutes;
 014        var slot = (int)Math.Floor(minutesAgo / slotMinutes);
 015        if (slot >= 0 && slot < timelineSlots)
 16        {
 017            timelineBuckets[timelineSlots - 1 - slot] += Math.Max(1, ev.Count);
 18        }
 19    }
 020    var timelineMax = timelineBuckets.Max();
 021    if (timelineMax == 0) timelineMax = 1;
 22
 023    var recentSearches = s.SearchEvents.Take(20).Reverse().ToList();
 024    var searchMaxMs = recentSearches.Count == 0 ? 1 : Math.Max(1, recentSearches.Max(q => q.DurationMs));
 25}
 26
 27<div class="job-logs-panel card mb-3 osj-panel" id="osj-panel">
 28    <div class="card-header py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
 29        <div>
 30            <strong><i class="fas fa-search text-primary me-1"></i>Поиск заказов — Elasticsearch</strong>
 31            <div class="small text-muted">Журнал индексации, скорость поиска и проверка документа в индексе</div>
 32        </div>
 33        <div class="d-flex gap-2 flex-wrap">
 34            <button type="button" class="btn btn-sm btn-outline-primary" id="osj-refresh-btn">
 35                <i class="fas fa-sync-alt me-1"></i>Обновить
 36            </button>
 37            <a href="/Studio2/Orders" class="btn btn-sm btn-outline-secondary">
 38                <i class="fas fa-list me-1"></i>К заказам
 39            </a>
 40        </div>
 41    </div>
 42    <div class="card-body">
 43        <div class="osj-intro mb-3">
 44            <i class="fas fa-circle-info"></i>
 45            <div>
 46                Данные журнала хранятся в памяти процесса приложения (последние операции с момента запуска).
 47                Счётчики индекса и checkpoint синхронизации — из Elasticsearch и БД.
 48            </div>
 49        </div>
 50
 51        <div class="job-logs-workflow">
 52            <div class="job-logs-workflow-step"><span class="job-logs-workflow-num">1</span><div><strong>Заказ меняется 
 53            <div class="job-logs-workflow-step"><span class="job-logs-workflow-num">2</span><div><strong>Инкрементальный
 54            <div class="job-logs-workflow-step"><span class="job-logs-workflow-num">3</span><div><strong>Поиск на «Заказ
 55        </div>
 56
 57        <div id="osj-drift-notice" class="osj-notice osj-notice-warning @(s.CountDrift > 0 ? "" : "d-none")" role="statu
 58            <div class="osj-notice-icon-wrap" aria-hidden="true">
 59                <i class="fas fa-triangle-exclamation"></i>
 60            </div>
 61            <div class="osj-notice-body">
 62                <div class="osj-notice-title">Расхождение ES ↔ MySQL</div>
 63                <div class="osj-notice-text">
 64                    Число документов в индексе не совпадает с таблицей <code>Orders</code>.
 65                    Поиск на главной может пропускать заказы или показывать устаревшие результаты.
 66                </div>
 67                <div class="osj-notice-metrics">
 68                    <div class="osj-notice-metric">
 69                        <span class="osj-notice-metric-label">|индекс − БД|</span>
 070                        <span class="osj-notice-metric-value @(s.IsDriftAlert ? "osj-metric-alert" : "")" id="osj-drift-
 71                    </div>
 72                    <div class="osj-notice-metric">
 73                        <span class="osj-notice-metric-label">Порог алерта</span>
 074                        <span class="osj-notice-metric-value" id="osj-drift-threshold">@s.DriftAlertThreshold.ToString("
 75                    </div>
 76                    <div class="osj-notice-metric">
 77                        <span class="osj-notice-metric-label">Индекс ES</span>
 078                        <span class="osj-notice-metric-value" id="osj-drift-index">@s.IndexCount.ToString("N0")</span>
 79                    </div>
 80                    <div class="osj-notice-metric">
 81                        <span class="osj-notice-metric-label">БД Orders</span>
 082                        <span class="osj-notice-metric-value" id="osj-drift-db">@s.DbOrderCount.ToString("N0")</span>
 83                    </div>
 84                </div>
 85                <div class="osj-notice-footer">
 086                    Проверка: <span id="osj-drift-checked">@(s.LastDriftCheckAt.HasValue ? ToMsk(s.LastDriftCheckAt.Valu
 87                    Метрики Prometheus: <code>studio2_order_search_drift_alert</code>, <code>studio2_order_search_count_
 88                </div>
 089                @if (User.IsInRole("Admin"))
 90                {
 91                    <div class="osj-notice-actions">
 92                        <button type="button" class="btn btn-sm btn-warning text-dark" id="osj-repair-btn-inline" title=
 93                            <i class="fas fa-wrench me-1"></i>Исправить расхождение
 94                        </button>
 95                        <button type="button" class="btn btn-sm btn-outline-secondary" id="osj-full-reindex-btn-inline" 
 96                            <i class="fas fa-redo me-1"></i>Полная переиндексация
 97                        </button>
 98                    </div>
 99                    <div class="osj-notice-footer mt-2">
 100                        Исправление расхождения — дешёвая операция, начинайте с неё.
 101                        Полная переиндексация тяжёлая: запускайте только при явной рассинхронизации.
 102                    </div>
 103                }
 104            </div>
 105        </div>
 106
 107        <div class="row row-cols-1 row-cols-md-2 row-cols-xl-4 g-2 mb-4" id="osj-stats-row">
 108            <div class="col">
 109                <div class="osj-stat-card h-100">
 110                    <div class="job-logs-stat">Статус ES</div>
 111                    <div class="osj-stat-value" id="osj-stat-es">
 0112                        @if (!s.EsEnabled)
 113                        {
 114                            <span class="badge bg-secondary">Выключен</span>
 115                        }
 0116                        else if (!s.IndexExists)
 117                        {
 118                            <span class="badge bg-danger">Индекс отсутствует</span>
 119                        }
 120                        else
 121                        {
 122                            <span class="badge bg-success">Активен</span>
 123                        }
 124                    </div>
 0125                    <div class="small text-muted mt-1" id="osj-stat-url">@s.EsUrl · @s.IndexName</div>
 126                </div>
 127            </div>
 128            <div class="col">
 129                <div class="osj-stat-card h-100 @(s.IsDriftAlert ? "osj-stat-card-alert" : "")">
 130                    <div class="job-logs-stat">Документов в индексе</div>
 0131                    <div class="osj-stat-value" id="osj-stat-index-count">@s.IndexCount.ToString("N0")</div>
 132                    <div class="small text-muted mt-1">
 0133                        В БД: <span id="osj-stat-db-count">@s.DbOrderCount.ToString("N0")</span>
 0134                        · |Δ|: <span id="osj-stat-drift">@s.CountDrift.ToString("N0")</span>
 135                    </div>
 136                </div>
 137            </div>
 138            <div class="col">
 139                <div class="osj-stat-card h-100">
 140                    <div class="job-logs-stat">Покрытие индекса</div>
 0141                    <div class="osj-stat-value" id="osj-stat-coverage">@Model.CoveragePercent.ToString("0.#")%</div>
 142                    <div class="progress osj-coverage-bar mt-2" style="height: 6px;">
 143                        <div class="progress-bar bg-success" id="osj-coverage-fill" style="width: @(Math.Min(100, Model.
 144                    </div>
 0145                    <div class="small text-muted mt-1">Отставание: <span id="osj-stat-gap">@Model.IndexGap.ToString("N0"
 146                </div>
 147            </div>
 148            <div class="col">
 149                <div class="osj-stat-card h-100">
 150                    <div class="job-logs-stat">Скорость поиска</div>
 151                    <div class="osj-stat-value">
 0152                        <span id="osj-stat-avg-ms">@s.AvgSearchMs.ToString("0.#")</span> мс
 153                        <span class="small text-muted">ср.</span>
 154                    </div>
 155                    <div class="small text-muted mt-1">
 0156                        Макс. <span id="osj-stat-max-ms">@s.MaxSearchMs</span> мс ·
 0157                        выборка <span id="osj-stat-search-count">@s.SearchSampleCount</span>
 158                    </div>
 159                </div>
 160            </div>
 161        </div>
 162
 163        <div class="row g-3 mb-4">
 164            <div class="col-lg-6">
 165                <div class="osj-chart-card h-100">
 166                    <div class="d-flex justify-content-between align-items-center mb-2">
 167                        <strong class="small text-uppercase text-muted">Индексация за час</strong>
 168                        <span class="badge bg-light text-dark border">по 5 мин</span>
 169                    </div>
 170                    <div class="osj-timeline" id="osj-timeline" aria-hidden="true">
 0171                        @for (var i = 0; i < timelineSlots; i++)
 172                        {
 0173                            var h = Math.Round(100.0 * timelineBuckets[i] / timelineMax, 1);
 0174                            var label = timelineStart.AddMinutes((i + 1) * slotMinutes);
 175                            <div class="osj-timeline-slot" title="@ToMsk(label).ToString("HH:mm") — @timelineBuckets[i] 
 176                                <div class="osj-timeline-bar" style="height: @(h)%"></div>
 177                            </div>
 178                        }
 179                    </div>
 180                    <div class="d-flex justify-content-between small text-muted mt-1 px-1">
 181                        <span>−60 мин</span>
 182                        <span>сейчас</span>
 183                    </div>
 184                </div>
 185            </div>
 186            <div class="col-lg-6">
 187                <div class="osj-chart-card h-100">
 188                    <div class="d-flex justify-content-between align-items-center mb-2">
 189                        <strong class="small text-uppercase text-muted">Длительность последних запросов</strong>
 190                        <span class="badge bg-light text-dark border">мс</span>
 191                    </div>
 192                    <div class="osj-search-bars" id="osj-search-bars">
 0193                        @if (recentSearches.Count == 0)
 194                        {
 195                            <div class="st2-empty-state st2-empty-state--compact"><img class="st2-empty-state__image" sr
 196                        }
 0197                        @foreach (var q in recentSearches)
 198                        {
 0199                            var w = Math.Round(100.0 * q.DurationMs / searchMaxMs, 1);
 0200                            var title = string.IsNullOrWhiteSpace(q.SearchText) ? q.FilterSummary : q.SearchText;
 201                            <div class="osj-search-bar-row" title="@title — @q.ResultCount результатов">
 202                                <div class="osj-search-bar-track">
 203                                    <div class="osj-search-bar-fill @(q.UsedFallback ? "osj-bar-fallback" : "")" style="
 204                                </div>
 0205                                <span class="osj-search-bar-ms">@q.DurationMs</span>
 206                            </div>
 207                        }
 208                    </div>
 209                </div>
 210            </div>
 211        </div>
 212
 213        <div class="osj-sync-meta row g-2 mb-4 small text-muted">
 214            <div class="col-md-6">
 215                <i class="fas fa-clock me-1"></i>Последний инкремент:
 0216                <strong id="osj-last-sync">@(s.LastSyncedAt.HasValue ? ToMsk(s.LastSyncedAt.Value).ToString("dd.MM.yyyy 
 217            </div>
 218            <div class="col-md-6">
 219                <i class="fas fa-redo me-1"></i>Полная переиндексация:
 0220                <strong id="osj-last-full">@(s.LastFullReindexAt.HasValue ? ToMsk(s.LastFullReindexAt.Value).ToString("d
 221            </div>
 222        </div>
 223
 224        <div class="osj-lookup card border mb-4">
 225            <div class="card-body py-3">
 226                <div class="row g-2 align-items-end">
 227                    <div class="col-md-2">
 228                        <label class="form-label small mb-1" for="osj-lookup-id">ID заказа</label>
 229                        <input type="number" class="form-control form-control-sm" id="osj-lookup-id" min="1" placeholder
 230                    </div>
 231                    <div class="col-md-4">
 232                        <label class="form-label small mb-1" for="osj-lookup-shipment">Номер отправления</label>
 233                        <input type="text" class="form-control form-control-sm" id="osj-lookup-shipment" placeholder="На
 234                    </div>
 235                    <div class="col-md-2">
 236                        <button type="button" class="btn btn-primary btn-sm w-100" id="osj-lookup-btn">
 237                            <i class="fas fa-magnifying-glass me-1"></i>Проверить
 238                        </button>
 239                    </div>
 240                </div>
 241                <div class="form-text">
 242                    Укажите ID или номер отправления — не оба поля сразу.
 243                    Сравним документ в ES с записью в БД и покажем расхождения по полям.
 244                </div>
 245                <div id="osj-lookup-result" class="mt-3" style="display: none;"></div>
 246            </div>
 247        </div>
 248
 249        <div class="row g-3">
 250            <div class="col-xl-6">
 251                <h6 class="mb-2"><i class="fas fa-scale-balanced me-1 text-danger"></i>Проверки расхождения ES ↔ MySQL</
 252                <div id="osj-drift-table" class="bsx-grid bsx-grid--auto"></div>
 253            </div>
 254            <div class="col-xl-6">
 255                <h6 class="mb-2"><i class="fas fa-database me-1 text-primary"></i>Журнал индексации</h6>
 256                <div id="osj-index-table" class="bsx-grid bsx-grid--auto"></div>
 257            </div>
 258            <div class="col-xl-6">
 259                <h6 class="mb-2"><i class="fas fa-bolt me-1 text-warning"></i>Последние поисковые запросы</h6>
 260                <div id="osj-search-table" class="bsx-grid bsx-grid--auto"></div>
 261            </div>
 262        </div>
 263    </div>
 264</div>
 265
 266<script>
 267    window.orderSearchJobLogsConfig = {
 0268        snapshotUrl: '@Url.Action("OrderSearchSnapshot", "JobLogs", new { area = "Studio2" })',
 0269        lookupUrl: '@Url.Action("OrderSearchLookup", "JobLogs", new { area = "Studio2" })',
 0270        repairDriftUrl: '@Url.Action("EnqueueOrderSearchDriftRepair", "JobLogs", new { area = "Studio2" })',
 0271        fullReindexUrl: '@Url.Action("EnqueueOrderSearchFullReindex", "JobLogs", new { area = "Studio2" })',
 0272        isAdmin: @(User.IsInRole("Admin") ? "true" : "false"),
 0273        initial: { driftEvents: @Json.Serialize(s.DriftEvents), indexEvents: @Json.Serialize(s.IndexEvents), searchEvent
 274    };
 275</script>
 276<script src="~/js/order-search-job-logs-tab.js" asp-append-version="true"></script>

Methods/Properties

ExecuteAsync()
ToMsk()