/* global React */
// ────────────────────────────────────────────────────────────
// DIGEST — экран «Сводка за период» (12h по умолчанию)
// Открывается через deep-link ?screen=digest или из боттом-таба.
// ────────────────────────────────────────────────────────────
window.DigestScreen = ({ T, dark, onBack, onOpenEvent, hours = 12 }) => {
const [period, setPeriod] = React.useState(hours); // 12 | 24
const [events, setEvents] = React.useState(null);
const [error, setError] = React.useState(null);
// Pull the user's preferred magnitude filter from the reactive store so the
// Сводка screen matches what's shown in Telegram's digest message.
const data = window.DATA.useDataState();
const minMag = data.userMagFilter ?? 4.0;
React.useEffect(() => {
let cancelled = false;
setEvents(null);
setError(null);
window.DATA.fetchEventsSince({ hours: period, minMag })
.then((rows) => { if (!cancelled) setEvents(rows); })
.catch((e) => { if (!cancelled) setError(String(e)); });
return () => { cancelled = true; };
}, [period, minMag]);
const now = new Date();
const since = new Date(now.getTime() - period * 3600 * 1000);
const fmtMsk = (d) => new Intl.DateTimeFormat("ru-RU", {
hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Europe/Moscow",
}).format(d);
const fmtDate = new Intl.DateTimeFormat("ru-RU", {
day: "2-digit", month: "2-digit", year: "numeric", timeZone: "Europe/Moscow",
}).format(now);
const periodMsk = `${fmtMsk(since)} — ${fmtMsk(now)} МСК`;
if (error) {
return (
);
}
if (events === null) {
return (
);
}
const total = events.length;
const ru = events.filter(e => e.isRu).length;
const world = total; // anything in BD is M≥4; "world" = total here
const m5plus = events.filter(e => e.mag >= 5.0).length;
const m6plus = events.filter(e => e.mag >= 6.0).length;
const m7plus = events.filter(e => e.mag >= 7.0).length;
const maxMag = total > 0 ? Math.max(...events.map(e => e.mag)) : 0;
const maxEvent = events.find(e => e.mag === maxMag);
// Top 5 by magnitude
const top5 = [...events].sort((a, b) => b.mag - a.mag).slice(0, 5);
// Hourly bins for the inline chart
const bins = makeHourlyBins(events, since, now);
const maxBin = Math.max(1, ...bins.map(b => b.count));
return (
{/* Период переключатель */}
{[12, 24].map(h => (
))}
{periodMsk}
{/* Сводка-карточки */}
{/* No-events placeholder */}
{total === 0 && (
За {period} часов событий M≥{minMag.toFixed(1)} нет
{period === 12
? "Попробуйте «24 ч» — обычно за сутки 3–10 событий."
: "Сейсмическая активность ниже среднего."}
{period === 12 && (
)}
)}
{/* Распределение M */}
{total > 0 && (
)}
{/* График по часам */}
Активность · число событий за час
Россия/СНГ
Мир
{/* Топ событий */}
{top5.length > 0 && (
ТОП ПО МАГНИТУДЕ
{top5.map((e, i, arr) => (
onOpenEvent(e)}
last={i === arr.length - 1} />
))}
)}
{/* Footer-инфо */}
ИСТОЧНИКИ: USGS · EMSC · мониторинг 24/7
);
};
// ────────── helpers ──────────
function makeHourlyBins(events, since, now) {
const totalMs = now.getTime() - since.getTime();
const totalHours = Math.max(1, Math.round(totalMs / 3600000));
const bins = [];
for (let i = 0; i < totalHours; i++) {
const t0 = new Date(since.getTime() + i * 3600000);
bins.push({ t: t0, count: 0, ru: 0 });
}
for (const e of events) {
const t = new Date(e.timeUtc);
const idx = Math.min(totalHours - 1,
Math.max(0, Math.floor((t.getTime() - since.getTime()) / 3600000)));
if (bins[idx]) {
bins[idx].count++;
if (e.isRu) bins[idx].ru++;
}
}
return bins;
}
// Responsive (viewBox-scaled) bar chart with proper axes.
// Layout (in viewBox units):
// left strip L_PAD = 28px → Y-axis tick labels (0, mid, max)
// bottom strip B_PAD = 18px → X-axis labels (start, mid, end times in МСК)
// plot area the rest of (W × H)
const HourlyBars = ({ T, bins, maxBin }) => {
const W = 360, H = 120;
const L_PAD = 28, R_PAD = 6, T_PAD = 6, B_PAD = 18;
const plotW = W - L_PAD - R_PAD;
const plotH = H - T_PAD - B_PAD;
const barW = bins.length > 0 ? plotW / bins.length : plotW;
// Y-axis ticks: 0, half, max — keep them as round integers when possible.
const yMax = Math.max(1, maxBin);
const yMid = Math.ceil(yMax / 2);
const tickFor = (v) => T_PAD + plotH - (v / yMax) * plotH;
// X-axis labels: first, middle, last hour in МСК.
const fmtTickHour = (d) => {
if (!d) return "";
return new Intl.DateTimeFormat("ru-RU", {
hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Europe/Moscow",
}).format(d);
};
const xLabels = bins.length > 0
? [bins[0].t, bins[Math.floor(bins.length / 2)].t, bins[bins.length - 1].t]
: [];
return (
);
};
const Pill = ({ T, k, v, accent, danger }) => {
const color = danger ? T.danger : accent ? T.accent : T.text;
return (
);
};
const DigestRow = ({ ev, T, onClick, last }) => (
{ev.place}
{ev.flag}
{ev.timeMsk} МСК
·
h={ev.depth.toFixed(1)} км
{ev.confirmed && ✓ 2 ист.}
);
function truncate(s, n) {
if (!s) return "";
return s.length <= n ? s : s.slice(0, n - 1) + "…";
}