arden scrape safety telemetry and polish run state UX
This commit is contained in:
parent
110e246a1c
commit
d7404abf18
33 changed files with 2074 additions and 96 deletions
|
|
@ -4,6 +4,7 @@ import {
|
|||
type PublicationMode,
|
||||
} from "@/features/publications";
|
||||
import { listQueueItems, listRuns, type RunListItem } from "@/features/runs";
|
||||
import { type ScrapeSafetyState } from "@/features/safety";
|
||||
|
||||
export interface QueueHealth {
|
||||
queued: number;
|
||||
|
|
@ -19,6 +20,7 @@ export interface DashboardSnapshot {
|
|||
recentRuns: RunListItem[];
|
||||
recentPublications: PublicationItem[];
|
||||
queue: QueueHealth;
|
||||
safetyState: ScrapeSafetyState;
|
||||
}
|
||||
|
||||
function countQueueStatuses(statuses: string[]): QueueHealth {
|
||||
|
|
@ -38,11 +40,12 @@ function countQueueStatuses(statuses: string[]): QueueHealth {
|
|||
}
|
||||
|
||||
export async function fetchDashboardSnapshot(): Promise<DashboardSnapshot> {
|
||||
const [publications, runs, queueItems] = await Promise.all([
|
||||
const [publications, runsPayload, queueItems] = await Promise.all([
|
||||
listPublications({ mode: "new", limit: 20 }),
|
||||
listRuns({ limit: 20 }),
|
||||
listQueueItems(200),
|
||||
]);
|
||||
const runs = runsPayload.runs;
|
||||
|
||||
const queueHealth = countQueueStatuses(queueItems.map((item) => item.status));
|
||||
|
||||
|
|
@ -54,5 +57,6 @@ export async function fetchDashboardSnapshot(): Promise<DashboardSnapshot> {
|
|||
recentRuns: runs,
|
||||
recentPublications: publications.publications,
|
||||
queue: queueHealth,
|
||||
safetyState: runsPayload.safety_state,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { apiRequest } from "@/lib/api/client";
|
||||
import { type ScrapeSafetyState } from "@/features/safety";
|
||||
|
||||
export interface RunListItem {
|
||||
id: number;
|
||||
|
|
@ -49,6 +50,7 @@ export interface RunDetail {
|
|||
run: RunListItem;
|
||||
summary: RunSummary;
|
||||
scholar_results: RunScholarResult[];
|
||||
safety_state: ScrapeSafetyState;
|
||||
}
|
||||
|
||||
export interface QueueItem {
|
||||
|
|
@ -68,6 +70,7 @@ export interface QueueItem {
|
|||
|
||||
interface RunsListData {
|
||||
runs: RunListItem[];
|
||||
safety_state: ScrapeSafetyState;
|
||||
}
|
||||
|
||||
interface QueueListData {
|
||||
|
|
@ -79,7 +82,7 @@ export interface RunsListQuery {
|
|||
limit?: number;
|
||||
}
|
||||
|
||||
export async function listRuns(query: RunsListQuery = {}): Promise<RunListItem[]> {
|
||||
export async function listRuns(query: RunsListQuery = {}): Promise<RunsListData> {
|
||||
const params = new URLSearchParams();
|
||||
if (query.failedOnly) {
|
||||
params.set("failed_only", "true");
|
||||
|
|
@ -92,7 +95,7 @@ export async function listRuns(query: RunsListQuery = {}): Promise<RunListItem[]
|
|||
const response = await apiRequest<RunsListData>(`/runs${suffix ? `?${suffix}` : ""}`, {
|
||||
method: "GET",
|
||||
});
|
||||
return response.data.runs;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getRunDetail(runId: number): Promise<RunDetail> {
|
||||
|
|
@ -118,6 +121,7 @@ export async function triggerManualRun(): Promise<{
|
|||
new_publication_count: number;
|
||||
reused_existing_run: boolean;
|
||||
idempotency_key: string | null;
|
||||
safety_state: ScrapeSafetyState;
|
||||
}> {
|
||||
const headers: Record<string, string> = {
|
||||
"Idempotency-Key": generateIdempotencyKey(),
|
||||
|
|
@ -133,6 +137,7 @@ export async function triggerManualRun(): Promise<{
|
|||
new_publication_count: number;
|
||||
reused_existing_run: boolean;
|
||||
idempotency_key: string | null;
|
||||
safety_state: ScrapeSafetyState;
|
||||
}>("/runs/manual", {
|
||||
method: "POST",
|
||||
headers,
|
||||
|
|
|
|||
112
frontend/src/features/safety/index.ts
Normal file
112
frontend/src/features/safety/index.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
export interface ScrapeSafetyCounters {
|
||||
consecutive_blocked_runs: number;
|
||||
consecutive_network_runs: number;
|
||||
cooldown_entry_count: number;
|
||||
blocked_start_count: number;
|
||||
last_blocked_failure_count: number;
|
||||
last_network_failure_count: number;
|
||||
last_evaluated_run_id: number | null;
|
||||
}
|
||||
|
||||
export interface ScrapeSafetyState {
|
||||
cooldown_active: boolean;
|
||||
cooldown_reason: string | null;
|
||||
cooldown_reason_label: string | null;
|
||||
cooldown_until: string | null;
|
||||
cooldown_remaining_seconds: number;
|
||||
recommended_action: string | null;
|
||||
counters: ScrapeSafetyCounters;
|
||||
}
|
||||
|
||||
export function createDefaultSafetyCounters(): ScrapeSafetyCounters {
|
||||
return {
|
||||
consecutive_blocked_runs: 0,
|
||||
consecutive_network_runs: 0,
|
||||
cooldown_entry_count: 0,
|
||||
blocked_start_count: 0,
|
||||
last_blocked_failure_count: 0,
|
||||
last_network_failure_count: 0,
|
||||
last_evaluated_run_id: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultSafetyState(): ScrapeSafetyState {
|
||||
return {
|
||||
cooldown_active: false,
|
||||
cooldown_reason: null,
|
||||
cooldown_reason_label: null,
|
||||
cooldown_until: null,
|
||||
cooldown_remaining_seconds: 0,
|
||||
recommended_action: null,
|
||||
counters: createDefaultSafetyCounters(),
|
||||
};
|
||||
}
|
||||
|
||||
function parseNumber(value: unknown, fallback: number): number {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function parseNullableString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
||||
}
|
||||
|
||||
export function normalizeSafetyState(value: unknown): ScrapeSafetyState {
|
||||
if (!value || typeof value !== "object") {
|
||||
return createDefaultSafetyState();
|
||||
}
|
||||
|
||||
const raw = value as Record<string, unknown>;
|
||||
const rawCounters = raw.counters;
|
||||
const counters = rawCounters && typeof rawCounters === "object"
|
||||
? (rawCounters as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
cooldown_active: Boolean(raw.cooldown_active),
|
||||
cooldown_reason: parseNullableString(raw.cooldown_reason),
|
||||
cooldown_reason_label: parseNullableString(raw.cooldown_reason_label),
|
||||
cooldown_until: parseNullableString(raw.cooldown_until),
|
||||
cooldown_remaining_seconds: Math.max(0, parseNumber(raw.cooldown_remaining_seconds, 0)),
|
||||
recommended_action: parseNullableString(raw.recommended_action),
|
||||
counters: {
|
||||
consecutive_blocked_runs: Math.max(0, parseNumber(counters.consecutive_blocked_runs, 0)),
|
||||
consecutive_network_runs: Math.max(0, parseNumber(counters.consecutive_network_runs, 0)),
|
||||
cooldown_entry_count: Math.max(0, parseNumber(counters.cooldown_entry_count, 0)),
|
||||
blocked_start_count: Math.max(0, parseNumber(counters.blocked_start_count, 0)),
|
||||
last_blocked_failure_count: Math.max(0, parseNumber(counters.last_blocked_failure_count, 0)),
|
||||
last_network_failure_count: Math.max(0, parseNumber(counters.last_network_failure_count, 0)),
|
||||
last_evaluated_run_id:
|
||||
counters.last_evaluated_run_id === null
|
||||
? null
|
||||
: Math.max(0, parseNumber(counters.last_evaluated_run_id, 0)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function formatCooldownCountdown(seconds: number): string {
|
||||
const bounded = Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0;
|
||||
if (bounded <= 0) {
|
||||
return "0s";
|
||||
}
|
||||
|
||||
const hours = Math.floor(bounded / 3600);
|
||||
const minutes = Math.floor((bounded % 3600) / 60);
|
||||
const secs = bounded % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
}
|
||||
return `${secs}s`;
|
||||
}
|
||||
|
|
@ -1,10 +1,31 @@
|
|||
import { apiRequest } from "@/lib/api/client";
|
||||
import { type ScrapeSafetyState } from "@/features/safety";
|
||||
|
||||
export interface UserSettingsPolicy {
|
||||
min_run_interval_minutes: number;
|
||||
min_request_delay_seconds: number;
|
||||
automation_allowed: boolean;
|
||||
manual_run_allowed: boolean;
|
||||
blocked_failure_threshold: number;
|
||||
network_failure_threshold: number;
|
||||
cooldown_blocked_seconds: number;
|
||||
cooldown_network_seconds: number;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
auto_run_enabled: boolean;
|
||||
run_interval_minutes: number;
|
||||
request_delay_seconds: number;
|
||||
nav_visible_pages: string[];
|
||||
policy: UserSettingsPolicy;
|
||||
safety_state: ScrapeSafetyState;
|
||||
}
|
||||
|
||||
export interface UserSettingsUpdate {
|
||||
auto_run_enabled: boolean;
|
||||
run_interval_minutes: number;
|
||||
request_delay_seconds: number;
|
||||
nav_visible_pages: string[];
|
||||
}
|
||||
|
||||
export interface ChangePasswordPayload {
|
||||
|
|
@ -18,7 +39,7 @@ export async function fetchSettings(): Promise<UserSettings> {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateSettings(payload: UserSettings): Promise<UserSettings> {
|
||||
export async function updateSettings(payload: UserSettingsUpdate): Promise<UserSettings> {
|
||||
const response = await apiRequest<UserSettings>("/settings", {
|
||||
method: "PUT",
|
||||
body: payload,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue