Files

151 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
// ---------- Анализ надёжности (инженеры и руководители) ----------
// Методика: «Надёжность и безопасность» (ИАТЭ, 2005), ГОСТ 27.002. Отказы — пуассоновский поток, λ ≈ const на участке
// нормальной эксплуатации; оценки по малоотказному опыту консервативные (доверие 90 %). Расчёты — на сервере (app/reliability.py).
const REL_KEY = "reliability_filters";
async function reliabilityPage(tab) {
tab = tab === "drift" ? "drift" : tab === "method" ? "method" : "summary";
const deps = await ref("departments");
let st;
try { st = JSON.parse(localStorage.getItem(REL_KEY) || "null"); } catch (e) { st = null; }
const today = new Date();
const ago = m => { const d = new Date(today); d.setMonth(d.getMonth() - m); return d; };
st = Object.assign({ from: dtIn(ago(12)), to: dtIn(today), department_id: "", cranes: "", confidence: "0.9", horizon: "30", days: "180" }, st || {});
const save = () => { try { localStorage.setItem(REL_KEY, JSON.stringify(st)); } catch (e) {} };
const form = h("div", { class: "card" }), result = h("div");
const params = extra => {
const q = new URLSearchParams(extra || {});
if (st.department_id) q.set("department_id", st.department_id);
if (st.cranes) q.set("cranes", st.cranes);
return q;
};
const periodQ = () => params({ from: st.from, to: st.to });
async function download(kind) {
const q = periodQ(); q.set("kind", kind);
const r = await authFetch(`${API}/reliability/export?${q}`);
if (!r.ok) { let d = r.statusText; try { d = (await r.json()).detail; } catch (_) {} throw new Error(d); }
const a = document.createElement("a"); a.href = URL.createObjectURL(await r.blob());
a.download = `${kind === "failures" ? "otkazy" : "obsluzhivanie_uzlov"}_${st.from.slice(0, 10)}_${st.to.slice(0, 10)}.csv`;
document.body.append(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}
function drawForm() {
const dt = (key, label) => field(label, h("input", { type: "datetime-local", value: st[key], onchange: e => { st[key] = e.target.value; save(); } }));
const quick = (label, m) => h("button", { class: "sec sm", onclick: () => { st.from = dtIn(ago(m)); st.to = dtIn(new Date()); save(); drawForm(); guard(show)(); } }, label);
form.replaceChildren(
tab === "drift" ? h("div", { class: "grid" },
field("За сколько дней смотреть значения", h("select", { onchange: e => { st.days = e.target.value; save(); } },
...[["90", "3 месяца"], ["180", "6 месяцев"], ["365", "год"], ["730", "2 года"]].map(([v, t]) => h("option", { value: v, selected: st.days === v }, t))))) :
h("div", { class: "grid" }, dt("from", "С"), dt("to", "По"),
field("Доверие оценок", h("select", { onchange: e => { st.confidence = e.target.value; save(); } },
...[["0.9", "90 % (консервативно)"], ["0.8", "80 %"], ["0.5", "50 % («средневероятно»)"]].map(([v, t]) => h("option", { value: v, selected: st.confidence === v }, t)))),
field("Прогноз на", h("select", { onchange: e => { st.horizon = e.target.value; save(); } },
...[["7", "неделю"], ["30", "месяц"], ["90", "квартал"], ["365", "год"]].map(([v, t]) => h("option", { value: v, selected: st.horizon === v }, t))))),
h("div", { class: "grid", style: "margin-top:8px" },
field("Отдел (с вложенными)", h("select", { onchange: e => { st.department_id = e.target.value; save(); } }, h("option", { value: "" }, "Все"),
...deps.map(d => h("option", { value: d.id, selected: String(d.id) === String(st.department_id) }, d.name)))),
me.crane_map ? field("Оборудование", h("select", { onchange: e => { st.cranes = e.target.value; save(); } },
...[["", "Всё"], ["false", "Без кранов"], ["true", "Только краны"]].map(([v, t]) => h("option", { value: v, selected: st.cranes === v }, t)))) : null),
tab === "drift" ? null : h("div", { class: "row", style: "margin-top:10px" }, h("span", { class: "small muted" }, "Период:"),
quick("3 месяца", 3), quick("6 месяцев", 6), quick("год", 12), quick("2 года", 24)),
h("div", { class: "row", style: "margin-top:12px" }, h("button", { onclick: guard(show) }, "Показать"),
tab === "drift" ? null : h("button", { class: "sec", title: "Одна строка на отказ или неисправность: времена, наработки, причины", onclick: guard(() => download("failures")) }, "⬇ CSV: отказы и неисправности"),
tab === "drift" ? null : h("button", { class: "sec", title: "Плановые и внеплановые замены узлов — «цензурированные» наблюдения для прогноза ресурса", onclick: guard(() => download("service")) }, "⬇ CSV: обслуживание узлов")));
}
const n1 = v => v == null ? "—" : (Math.round(v * 10) / 10).toLocaleString("ru-RU");
const n0 = v => v == null ? "—" : Math.round(v).toLocaleString("ru-RU");
const pct = v => v == null ? "—" : (Math.round(v * 1000) / 10).toLocaleString("ru-RU") + " %";
const per1000 = v => v == null ? "—" : (Math.round(v * 1000 * 100) / 100).toLocaleString("ru-RU");
const bars = (title, list, hint) => h("div", { class: "card" }, h("h2", { style: "margin-top:0" }, title), hint ? h("p", { class: "small muted" }, hint) : null,
list.length ? h("div", { class: "relbars" }, ...list.map(x => h("div", { class: "relbar" }, h("span", { class: "rl" }, x.name),
h("span", { class: "rb" }, h("i", { style: `width:${Math.max(3, x.pct ?? 0)}%` })), h("b", {}, x.n, x.pct != null ? h("span", { class: "small muted" }, ` · ${x.pct}%`) : null))))
: h("p", { class: "small muted" }, "Нет данных за период"));
async function show() {
if (tab === "method") return;
if (tab === "drift") return showDrift();
const q = periodQ(); q.set("confidence", st.confidence); q.set("horizon_days", st.horizon);
const j = await api("GET", "/reliability?" + q);
const F = j.fleet, conf = Math.round(j.confidence * 100);
const tile = (n, label, cls, title) => h("div", { class: "stat " + (cls || ""), title }, h("b", {}, n), h("span", {}, label));
const stats = h("div", { class: "stats" },
tile(F.machines, "машин в анализе"), tile(F.failures, "отказов", F.failures ? "relbad" : ""), tile(F.faults, "сообщений о неисправности"),
tile(F.escalated, "неисправность → отказ", F.escalated ? "relwarn" : "", "Сколько сообщений о неисправности переросли в отказ"),
tile(n0(F.theta), "наработка на отказ, ч (оценка)"), tile(n0(F.theta_lower), `не менее, ч (доверие ${conf} %)`),
tile(n1(F.mttr), "среднее восстановление, ч"), tile(n1(F.downtime), "простой всего, ч"), tile(F.without_failures, "без отказов"));
const help = h("p", { class: "small muted" }, `θ — средняя наработка на отказ (наработка ÷ число отказов). «θ не менее» — консервативная нижняя граница при доверии ${conf} %: `,
"даже без отказов даёт оценку по накопленной наработке (в 2,3 раза меньше её при 90 %). Kг — коэффициент готовности θ / (θ + Tв). ",
`Прогноз — вероятность хотя бы одного отказа за ${j.horizon_days} дн. при текущей интенсивности использования (поток Пуассона). `,
"«*» — нет показаний счётчика, взято календарное время.");
const tableRows = j.rows.map(r => h("tr", { class: "click", onclick: () => location.hash = "#/equipment/" + r.equipment_id },
h("td", {}, h("b", {}, r.equipment), h("div", { class: "small muted" }, [r.inventory_no, r.location, r.department].filter(Boolean).join(" · "))),
h("td", {}, n0(r.hours), r.basis === "calendar" ? h("span", { class: "muted", title: "Нет показаний счётчика — календарное время" }, "*") : null),
h("td", {}, r.failures ? h("b", { class: "s-due" }, r.failures) : "0"),
h("td", {}, r.faults || "—", r.escalated ? h("div", { class: "small", style: "color:var(--bad)" }, `→ отказ: ${r.escalated}`) : null),
h("td", {}, n0(r.theta)), h("td", {}, n0(r.theta_lower)), h("td", { title: "отказов на 1000 ч (верхняя граница)" }, per1000(r.lambda), h("div", { class: "small muted" }, "≤ " + per1000(r.lambda_upper))),
h("td", {}, n1(r.mttr), r.wait_mean ? h("div", { class: "small muted", title: "из них ожидание запчастей" }, "запч. " + n1(r.wait_mean)) : null),
h("td", {}, pct(r.availability ?? r.availability_period)), h("td", {}, n1(r.downtime)),
h("td", {}, h("b", { class: r.forecast_p >= 0.5 ? "s-due" : r.forecast_p >= 0.2 ? "s-soon" : "s-ok" }, pct(r.forecast_p)))));
const table = h("div", { class: "card tablewrap" }, h("h2", { style: "margin-top:0" }, "По машинам"), help,
j.rows.length ? h("table", { class: "reltable" }, h("thead", {}, h("tr", {}, ...["Машина", "Наработка, ч", "Отказы", "Неиспр.", "θ, ч", `θ ≥ (${conf} %)`, "λ на 1000 ч",
"Tв, ч", "Kг", "Простой, ч", `Прогноз ${j.horizon_days} дн.`].map(t => h("th", {}, t)))), h("tbody", {}, ...tableRows))
: h("p", { class: "muted" }, "Нет оборудования по фильтру"));
const maxM = Math.max(1, ...j.months.map(m => m.n));
const months = h("div", { class: "card" }, h("h2", { style: "margin-top:0" }, "Отказы по месяцам"),
j.months.length ? h("div", { class: "relmonths" }, ...j.months.map(m => h("div", { class: "rm", title: `${m.month}: ${m.n}` },
h("i", { style: `height:${Math.round(100 * m.n / maxM)}%` }), h("b", {}, m.n), h("span", {}, m.month.slice(5) + "." + m.month.slice(2, 4)))))
: h("p", { class: "small muted" }, "Отказов за период не было"));
const topMax = Math.max(1, ...j.cause_units.map(y => y.n));
fill(result, stats, table,
h("div", { class: "relgrid" },
bars("Причины отказов", j.causes, "Если много «не установлена» — уточняйте причину при закрытии отказа"),
bars("Что сделано", j.actions), bars("Характер", j.natures),
bars("Были ли признаки заранее", j.precursors, "«Было сообщение о неисправности» и «замечено на обходе» — отказы, которые можно было предотвратить"),
bars("Узлы-причины (топ)", j.cause_units.map(x => ({ ...x, pct: Math.round(100 * x.n / topMax) }))),
months));
}
async function showDrift() {
const q = params({ days: st.days });
const list = await api("GET", "/reliability/drift?" + q);
const risky = list.filter(r => r.risk);
const fmtB = r => r.bound === "max" ? `↑ к максимуму ${r.max}` : r.bound === "min" ? `↓ к минимуму ${r.min}` : "—";
fill(result, h("div", { class: "card" }, h("h2", { style: "margin-top:0" }, `Параметры под наблюдением: ${risky.length} из ${list.length}`),
h("p", { class: "small muted" }, "Модель «параметр — граница работоспособности»: по числовым пунктам чек-листов с нормой строится линейный тренд, ",
"считается, через сколько дней значение дойдёт до границы, и сколько раз оно уже выходило за неё (ν — выходов за 30 дней). ",
"Так можно поймать постепенный отказ до того, как он случится. Нужно хотя бы 3 значения."),
list.length ? h("div", { class: "tablewrap" }, h("table", { class: "reltable" },
h("thead", {}, h("tr", {}, ...["Машина", "Пункт", "Норма", "Последнее", "Тренд за 30 дн.", "Куда", "До границы, дн.", "Выходы за норму", "Замеров"].map(t => h("th", {}, t)))),
h("tbody", {}, ...list.map(r => h("tr", { class: r.risk ? "relrisk" : "" },
h("td", {}, h("a", { href: "#/equipment/" + r.equipment_id }, r.equipment)), h("td", {}, r.item),
h("td", {}, `${r.min ?? "…"} – ${r.max ?? "…"} ${r.unit}`), h("td", {}, `${r.last} ${r.unit}`, h("div", { class: "small muted" }, fmt(r.last_at))),
h("td", {}, (r.trend_per_30d > 0 ? "+" : "") + r.trend_per_30d), h("td", {}, fmtB(r)),
h("td", {}, r.days_to_bound == null ? "—" : h("b", { class: r.days_to_bound <= 30 ? "s-due" : r.days_to_bound <= 90 ? "s-soon" : "" }, n0(r.days_to_bound))),
h("td", {}, r.out_of_range ? h("b", { class: "s-due" }, r.out_of_range, h("span", { class: "small muted" }, ` · ν=${r.nu_per_30d}`)) : "—"),
h("td", {}, r.n))))))
: h("p", { class: "muted" }, "Нет числовых пунктов с нормой и достаточным числом замеров")));
}
function method() {
fill(result, h("div", { class: "card relmethod" }, h("h2", { style: "margin-top:0" }, "Как считается"),
h("ul", {},
h("li", {}, h("b", {}, "Отказ"), " — оборудование остановлено (запись «Отказ»). «Неисправность» — проблема есть, но машина работает; в показатели надёжности не входит, но учитывается, если перешла в отказ."),
h("li", {}, h("b", {}, "Наработка"), " — по показаниям счётчика моточасов за период; если их нет, берётся календарное время (машина считается работавшей круглосуточно — оценка консервативная)."),
h("li", {}, h("b", {}, "Интенсивность отказов"), " λ = n / T и ", h("b", {}, "наработка на отказ"), " θ = T / n (поток отказов — пуассоновский, участок нормальной эксплуатации, λ ≈ const)."),
h("li", {}, h("b", {}, "Оценка по малоотказному опыту:"), " верхняя граница λ при доверии Pα — χ²(Pα; 2n+2) / 2T. Без отказов при 90 %: λ ≤ 2,3 / T, то есть θ ≥ T / 2,3. При 50 % — «средневероятная» оценка θ ≈ T / 0,69."),
h("li", {}, h("b", {}, "Время восстановления"), " Tв — от отказа (или перевода неисправности в отказ) до ввода в эксплуатацию; отдельно видно ожидание запчастей. ",
h("b", {}, "Коэффициент готовности"), " Kг = θ / (θ + Tв)."),
h("li", {}, h("b", {}, "Прогноз:"), " ожидаемое число отказов μ = λ × наработка за горизонт (по средней загрузке машины), вероятность хотя бы одного отказа 1 − e^(−μ). Если отказов не было, берётся «средневероятная» λ."),
h("li", {}, h("b", {}, "Дрейф параметров:"), " модель «параметр — граница работоспособности»: линейный тренд числового пункта чек-листа, прогноз дня выхода за норму и частота выходов ν."),
h("li", {}, h("b", {}, "Выгрузки CSV"), " — для собственных расчётов: строка на каждый отказ и неисправность и история обслуживания узлов (плановые замены — «цензурированные» наблюдения: узел заменили, не дождавшись отказа)."),
h("li", {}, "Абсолютные цифры приблизительны (мало данных, допущения в запас). Главная польза — ", h("b", {}, "сравнение машин"), " и поиск слабых мест на одной методике."))));
}
fill($app, h("h1", {}, "Анализ надёжности"),
tabs([["#/reliability", "Показатели"], ["#/reliability/drift", "Дрейф параметров"], ["#/reliability/method", "Как считается"]], "#/reliability" + (tab === "summary" ? "" : "/" + tab)),
tab === "method" ? null : form, result);
if (tab === "method") return method();
drawForm();
await show();
}