[794-5][@ant] fix: current = high-water mark парсера + относительный API base — честный статус индекса
CI / build (push) Failing after 7s
CI / build (push) Failing after 7s
Два дефекта отображения синка: 1. SPA: new URL(path, '') бросал TypeError при пустом VITE_API_BASE → health-проба всегда null → «индекс недоступен», хотя бэкенд synced. Фолбэк base на window.location.origin (vite-proxy/nginx шлёт /v1 на api). vite.config — фикс-порт 5174 (strictPort) + proxy /v1 → :3030, наружу один ssh-проброс. 2. health.current_block брался из последней строки permission_history → «висел» на блоке последней смены прав (6303) в тихие периоды и читался как зависание. Теперь current = block_num tip-записи стрима (getParserHead) = докуда реально обработал парсер; растёт у головы в реальном времени. Побочно чинит C6: beyond_index сравнивал at с устаревшим current_block_time (31 мая) и ложно отбраковывал свежие запросы.
This commit is contained in:
+13
-22
@@ -2,7 +2,7 @@ import type { Kysely } from 'kysely';
|
||||
import type { Database, KeyWeight } from './db/schema.js';
|
||||
import { SYNC_KEY } from './config.js';
|
||||
import { classify, type HealthView } from './services/health-status.js';
|
||||
import { getStreamLag } from './services/stream-lag.js';
|
||||
import { getStreamLag, getParserHead } from './services/stream-lag.js';
|
||||
|
||||
export interface CheckResultRow {
|
||||
active: boolean;
|
||||
@@ -89,32 +89,23 @@ export async function getHealth(db: Kysely<Database>): Promise<HealthView> {
|
||||
.where('key', '=', SYNC_KEY)
|
||||
.executeTakeFirst();
|
||||
|
||||
const { lag } = await getStreamLag();
|
||||
const [{ lag }, parser] = await Promise.all([getStreamLag(), getParserHead()]);
|
||||
|
||||
if (!p) {
|
||||
return {
|
||||
head_block: 0,
|
||||
head_block_time: null,
|
||||
current_block: 0,
|
||||
current_block_time: null,
|
||||
behind_by: 0,
|
||||
stream_lag: lag,
|
||||
status: classify(lag),
|
||||
bootstrap_done: false,
|
||||
source: 'verifier-indexer',
|
||||
};
|
||||
}
|
||||
// current = high-water mark парсера (докуда реально обработано), НЕ последняя запись
|
||||
// permission_history — иначе в тихие периоды current «висит» на блоке последней смены прав.
|
||||
const head = p ? Number(p.head_block_num) : 0;
|
||||
const current = parser.block_num ?? (p ? Number(p.block_num) : 0);
|
||||
const currentTime = parser.block_time ?? (p ? toIso(p.block_time) : null);
|
||||
|
||||
const behind_by = Math.max(0, Number(p.head_block_num) - Number(p.block_num));
|
||||
return {
|
||||
head_block: Number(p.head_block_num),
|
||||
head_block_time: toIso(p.head_block_time),
|
||||
current_block: Number(p.block_num),
|
||||
current_block_time: toIso(p.block_time),
|
||||
behind_by,
|
||||
head_block: head,
|
||||
head_block_time: p ? toIso(p.head_block_time) : null,
|
||||
current_block: current,
|
||||
current_block_time: currentTime,
|
||||
behind_by: Math.max(0, head - current),
|
||||
stream_lag: lag,
|
||||
status: classify(lag),
|
||||
bootstrap_done: p.bootstrap_done,
|
||||
bootstrap_done: p ? p.bootstrap_done : false,
|
||||
source: 'verifier-indexer',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,6 +58,41 @@ export async function getStreamLag(): Promise<StreamLag> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ParserHead {
|
||||
/** Последний блок, который парсер обработал и записал в стрим (high-water mark). */
|
||||
block_num: number | null;
|
||||
/** Время этого блока (UTC ISO). */
|
||||
block_time: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* High-water mark парсера = block_num последней записи стрима (tip).
|
||||
*
|
||||
* Зачем отдельно от permission-прогресса: индексатор пишет в БД только при смене прав,
|
||||
* поэтому «последняя строка permission_history» застывает в тихие периоды и читается как
|
||||
* зависание. Честное «докуда обработано» — то, что парсер уже опубликовал в стрим; consumer
|
||||
* с lag=0 стоит на том же tip. Эта величина и есть UI-`current` и граница beyond_index для C6.
|
||||
*/
|
||||
export async function getParserHead(): Promise<ParserHead> {
|
||||
try {
|
||||
const raw = (await client().call('XREVRANGE', streamKey(), '+', '-', 'COUNT', '1')) as unknown[];
|
||||
const entry = raw[0] as [string, string[]] | undefined;
|
||||
if (!entry) return { block_num: null, block_time: null };
|
||||
const fields = pairs(entry[1]);
|
||||
const data = typeof fields.data === 'string' ? JSON.parse(fields.data) : null;
|
||||
if (!data || typeof data.block_num !== 'number') return { block_num: null, block_time: null };
|
||||
return { block_num: data.block_num, block_time: toUtc(data.block_time) };
|
||||
} catch {
|
||||
return { block_num: null, block_time: null };
|
||||
}
|
||||
}
|
||||
|
||||
/** block_time из цепи приходит без таймзоны (`2026-06-03T16:56:03.500`) — это UTC, помечаем Z. */
|
||||
function toUtc(t: unknown): string | null {
|
||||
if (typeof t !== 'string' || t.length === 0) return null;
|
||||
return /[zZ]|[+-]\d\d:?\d\d$/.test(t) ? t : `${t}Z`;
|
||||
}
|
||||
|
||||
export async function closeStreamLag(): Promise<void> {
|
||||
if (redis) {
|
||||
await redis.quit().catch(() => {});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:3030';
|
||||
// Пустой VITE_API_BASE => относительные запросы от origin страницы (vite-proxy/прод-nginx
|
||||
// шлёт /v1 на api). new URL() с пустым base бросает TypeError, поэтому фолбэк на origin.
|
||||
const API_BASE =
|
||||
import.meta.env.VITE_API_BASE || (typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
export interface ActiveCheckResponse {
|
||||
active: boolean;
|
||||
|
||||
+11
-1
@@ -2,7 +2,17 @@ import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
// Фиксированный порт 5174 (strictPort — не «уползать» на 5175, чтобы ssh-проброс был стабилен).
|
||||
// /v1 проксируется на локальный verifier-api :3030 — наружу нужен ОДИН проброс (5174),
|
||||
// фронт ходит относительным путём (VITE_API_BASE='' в .env.local).
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
server: { port: 5173 },
|
||||
server: {
|
||||
host: true,
|
||||
port: 5174,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/v1': { target: 'http://localhost:3030', changeOrigin: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user