feat: expose safe source health status
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import "../styles/catalog.css";
|
||||
import "../styles/source-status.css";
|
||||
import FishingIcon from "../components/FishingIcon.astro";
|
||||
const {
|
||||
title = "RF4 Spotter — свежие точки клёва Russian Fishing 4",
|
||||
@@ -56,6 +57,6 @@ const jsonLd = JSON.stringify({
|
||||
<p class="live-badge"><span></span> Свежие данные и честная оценка</p>
|
||||
</header>
|
||||
<main id="main-content" tabindex="-1"><slot /></main>
|
||||
<footer><a href="/" class="brand"><span class="brand-mark"><FishingIcon name="hook" size={24}/></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a><p>Неофициальный проект для игроков Russian Fishing 4. <a href="/rules">Правила</a> · <a href="/privacy">Конфиденциальность</a></p><span>НХНЧ · 2026</span></footer>
|
||||
<footer><a href="/" class="brand"><span class="brand-mark"><FishingIcon name="hook" size={24}/></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a><p>Неофициальный проект для игроков Russian Fishing 4. <a href="/status">Статус</a> · <a href="/rules">Правила</a> · <a href="/privacy">Конфиденциальность</a></p><span>НХНЧ · 2026</span></footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,6 +12,7 @@ export type DictionaryItem = { id: string; slug: string; name_ru: string };
|
||||
export type OfficialRecord = { id: string; fish: string; weight_g: number; waterbody: string; bait: string | null; player_name: string | null; record_date: string | null; category: string | null; region: string | null; source_url: string | null; source_system: string };
|
||||
export type PublicObservation = { id: string; source_system: string; source_name: string; source_url: string; fish_name: string; waterbody_name: string; x: number | null; y: number | null; weight_g: number | null; last_seen_at: string; missing_fields: string[]; quality: "incomplete" | "unverified" };
|
||||
export type ImportRun = { id: string; started_at: string; finished_at: string | null; status: string; source_url: string; rows_seen: number; rows_created: number; rows_updated: number; error_summary: string | null };
|
||||
export type SourceStatus = { source_system: string; name: string; status: "healthy" | "stale" | "temporarily_limited" | "source_changed" | "waiting" | "disabled"; last_started_at: string | null; last_success_at: string | null; observations: number };
|
||||
|
||||
export const spotPath = (item: Pick<Activity, "waterbody_slug" | "x" | "y">) => `/spots/${item.waterbody_slug}-${item.x}x${item.y}`;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const escapeXml = (value: string) => value.replace(/[<>&'\"]/g, character => ({
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const origin = site?.origin ?? import.meta.env.PUBLIC_SITE_URL ?? "https://rf4spotter.ru";
|
||||
const paths = new Set(["/", "/records", "/report", "/rules", "/privacy"]);
|
||||
const paths = new Set(["/", "/records", "/report", "/status", "/rules", "/privacy"]);
|
||||
try {
|
||||
const [activity, fishes, waters] = await Promise.all([api<Activity[]>("/api/v1/activity?hours=72&limit=100"), api<DictionaryItem[]>("/api/v1/fishes?limit=500"), api<DictionaryItem[]>("/api/v1/waterbodies?limit=500")]);
|
||||
paths.add("/fish"); paths.add("/waterbodies");
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import SourceBadge from "../components/SourceBadge.astro";
|
||||
import { ago, api, plural, type SourceStatus } from "../lib/api";
|
||||
let sources: SourceStatus[] = [], unavailable = false;
|
||||
try { sources = await api<SourceStatus[]>("/api/v1/source-status"); } catch { unavailable = true; }
|
||||
const labels = { healthy:"Актуален", stale:"Данные устарели", temporarily_limited:"Временная пауза", source_changed:"Источник изменился", waiting:"Ожидает запуска", disabled:"Выключен" };
|
||||
---
|
||||
<Layout title="Состояние источников — RF4 Spotter" description="Текущее безопасное состояние источников данных RF4 Spotter.">
|
||||
<section class="catalog-hero content-grid"><span class="overline">Прозрачность данных</span><h1>Статус источников</h1><p>Пауза или изменение страницы не удаляют уже опубликованные данные.</p></section>
|
||||
{unavailable ? <div class="state content-grid"><h2>Статус временно недоступен</h2></div> : <section class="source-health-grid content-grid">{sources.map(source => <article data-status={source.status}><div><SourceBadge source={source.source_system}/><span class="health-state"><i></i>{labels[source.status]}</span></div><h2>{source.name}</h2><strong>{source.observations} {plural(source.observations,["наблюдение","наблюдения","наблюдений"])}</strong><p>{source.last_success_at ? `Последнее успешное обновление ${ago(source.last_success_at)}` : "Успешных запусков пока нет"}</p></article>)}</section>}
|
||||
</Layout>
|
||||
@@ -0,0 +1 @@
|
||||
.source-health-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:13px;padding:34px 0 100px}.source-health-grid article{padding:21px;border:1px solid #d3ddd5;border-radius:14px;background:#fff}.source-health-grid article>div{display:flex;justify-content:space-between;gap:8px}.source-health-grid h2{font:400 23px Georgia,serif;margin:18px 0 6px}.source-health-grid strong{font-size:13px}.source-health-grid p{color:#526662;font-size:12px}.health-state{display:flex;align-items:center;gap:6px;font-size:10px;font-weight:750;color:#526662}.health-state i{width:8px;height:8px;border-radius:50%;background:#c59a36}.source-health-grid [data-status="healthy"] .health-state i{background:#79aa36}.source-health-grid [data-status="source_changed"] .health-state i{background:#bc5748}.source-health-grid [data-status="disabled"]{opacity:.65}@media(max-width:900px){.source-health-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:620px){.source-health-grid{grid-template-columns:1fr}}
|
||||
Reference in New Issue
Block a user