TypeScript 55.4%
Python 43.2%
SQL 1.2%
1/**2 * Typed gateway client. Server Components call the gateway over loopback (`API_URL`);3 * client components use same-origin `/api/v1` (or `NEXT_PUBLIC_API_URL` in dev).4 * Every server helper swallows failures and returns an empty shape so pages render5 * "warming up" states instead of crashing.6 */78export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.websensor.io";910export function serverApiBase(): string {11 return process.env.API_URL ?? "http://127.0.0.1:8260";12}1314export function clientApiBase(): string {15 if (typeof window === "undefined") return serverApiBase();16 return process.env.NEXT_PUBLIC_API_URL ?? "";17}1819// ---------------------------------------------------------------------------------------20// Types (snake_case = REST rows; camelCase = WebSocket payloads)21// ---------------------------------------------------------------------------------------2223export interface EntityRef {24 id: string;25 name: string;26 type: string;27 role?: string;28}2930export interface SourceRef {31 id: string;32 name: string;33 domain: string;34 tier?: string;35 categories?: string[];36}3738export interface SensorRef {39 id: string;40 name: string;41 type: string;42 connector?: string;43 tier?: string;44}4546export interface EventItem {47 id: string;48 slug: string;49 event_type: string;50 title: string;51 summary: string;52 why_it_matters?: string | null;53 importance: number;54 confidence: number;55 novelty: number;56 categories: string[];57 keywords: string[];58 silent_change: boolean;59 evidence_label: string;60 url: string;61 detected_at: string;62 published_at?: string | null;63 observed_from?: string | null;64 processed_at?: string | null;65 published_to_feed_at?: string | null;66 detection_latency_ms?: number | null;67 processing_latency_ms?: number | null;68 cluster_id?: string | null;69 sensor_id: string;70 source_id: string;71 change_id?: string | null;72 old_snapshot_id?: string | null;73 new_snapshot_id?: string | null;74 importance_components?: Record<string, number> | null;75 processing_version?: string;76 interpretation?: Record<string, unknown> | null;77 source: SourceRef;78 sensor: SensorRef;79 entities: EntityRef[];80 cluster_size?: number | null;81 // 0.2 intelligence fields82 signal_score?: number | null;83 velocity_score?: number | null;84 impact_score?: number | null;85 anomaly_score?: number | null;86 change_class?: string | null;87 first_party?: boolean;88 country?: string | null;89 language?: string | null;90 canonical_url?: string | null;91 field_changes?: FieldChange[] | null;92 score_reasons?: ScoreReason[] | null;93 cluster?: ClusterRef | null;94}9596export interface FieldChange {97 label: string;98 kind: "price" | "percent" | "number" | "date" | "version" | "status" | "text";99 before: string | null;100 after: string | null;101 deltaPct?: number | null;102}103104export interface ScoreReason {105 sign: "+" | "-";106 text: string;107 points?: number;108}109110export interface ClusterRef {111 id: string;112 slug?: string | null;113 state?: string;114 event_count: number;115 source_count?: number;116 first_party_count?: number;117 external_count?: number;118 velocity?: number;119 lead_time_ms?: number | null;120}121122export interface LiveEvent {123 id: string;124 slug: string;125 type: string;126 title: string;127 summary: string;128 importance: number;129 confidence: number;130 novelty: number;131 silent: boolean;132 evidence: string;133 source: SourceRef;134 sensor: SensorRef;135 entities: EntityRef[];136 categories: string[];137 url: string;138 clusterId?: string | null;139 clusterSlug?: string | null;140 clusterSize?: number;141 clusterState?: string;142 detectedAt: string;143 publishedAt?: string | null;144 signal?: number;145 impact?: number;146 velocity?: number;147 firstParty?: boolean;148 country?: string | null;149 language?: string | null;150 changeClass?: string | null;151 fieldChanges?: FieldChange[];152 group?: string;153 sid?: string;154 replayed?: boolean;155}156157/** Normalize a WebSocket payload to the REST row shape used by all list components. */158export function liveToEvent(e: LiveEvent): EventItem {159 return {160 id: e.id,161 slug: e.slug,162 event_type: e.type,163 title: e.title,164 summary: e.summary,165 importance: e.importance,166 confidence: e.confidence,167 novelty: e.novelty,168 categories: e.categories ?? [],169 keywords: [],170 silent_change: Boolean(e.silent),171 evidence_label: e.evidence ?? "OBSERVED",172 url: e.url,173 detected_at: e.detectedAt,174 published_at: e.publishedAt ?? null,175 cluster_id: e.clusterId ?? null,176 sensor_id: e.sensor?.id ?? "",177 source_id: e.source?.id ?? "",178 source: e.source,179 sensor: e.sensor,180 entities: e.entities ?? [],181 cluster_size: e.clusterSize ?? 1,182 cluster: e.clusterId ? { id: e.clusterId, slug: e.clusterSlug ?? null, state: e.clusterState, event_count: e.clusterSize ?? 1, velocity: e.velocity } : null,183 signal_score: e.signal ?? null,184 velocity_score: e.velocity ?? 0,185 impact_score: e.impact ?? 0,186 change_class: e.changeClass ?? null,187 first_party: e.firstParty !== false,188 country: e.country ?? null,189 language: e.language ?? null,190 field_changes: e.fieldChanges ?? null,191 };192}193194export interface EventsPage {195 items: EventItem[];196 nextCursor: string | null;197}198199export interface CoverageSectorRow {200 sector: string;201 label: string;202 description: string | null;203 weight: number;204 members: number;205 covered: number;206 shadow_members: number;207 queued: number;208 sensors: number;209 breadth: number;210 depth: number;211 score: number;212}213export interface CoverageSummary {214 global_score: number;215 members: number;216 covered: number;217 sensors_matched: number;218 sectors: CoverageSectorRow[];219 monitored: { sources: number; sensors: number; shadow: number; countries: number };220 factory: Record<string, number>;221 method: string;222 issues: string[];223 generated_at: string;224}225export interface CoverageMemberRow {226 name: string;227 domain: string;228 country: string | null;229 importance: number;230 universe: string;231 covered: boolean;232 sensors: number;233 shadow: number;234 source_id: string | null;235 seed_status: string | null;236 depth: number;237}238export interface CoverageUniverseRow {239 key: string;240 label: string;241 description: string | null;242 provenance: string | null;243 members: number;244 covered: number;245 sensors: number;246 breadth: number;247 depth: number;248 score: number;249 items: CoverageMemberRow[];250}251export interface CoverageSectorDetail extends CoverageSectorRow {252 categories: string[];253 universes: CoverageUniverseRow[];254 by_country: { country: string; members: number; covered: number }[];255 generated_at: string;256}257258export interface Stats {259 sensors_shadow?: number;260 sources_factory?: number;261 checks_per_min?: number;262 events_per_min?: number;263 events_1h?: number;264 breaking_now?: number;265 developing_now?: number;266 sources_first_party?: number;267 countries?: number;268 not_modified_ratio_5m?: number | null;269 sources?: number;270 sensors?: number;271 entities?: number;272 snapshots?: number;273 events_total?: number;274 events_24h?: number;275 silent_24h?: number;276 breaking_24h?: number;277 changes_24h?: number;278 checks_today?: number;279 not_modified_today?: number;280 bytes_today?: number;281 checks_last_hour?: number;282 sensors_up?: number;283 sensors_degraded?: number;284 last_check_at?: string | null;285 last_event_at?: string | null;286 p50_processing_ms?: number | null;287 p50_detection_ms?: number | null;288}289290export interface TrendingItem {291 id: string;292 name: string;293 type: string;294 domain?: string | null;295 events: number;296 importance_sum: number;297 sources: number;298 silent: number;299 max_importance: number;300 prev_events: number;301 score: number;302 first_party?: number;303 confirmed?: number;304 avg_signal?: number;305 baseline_per_day?: number;306 direction?: "up" | "down" | "flat";307 entity_importance?: number;308}309310export interface Cluster {311 id: string;312 title: string;313 summary?: string | null;314 primary_event_id?: string | null;315 entity_ids: string[];316 categories: string[];317 event_count: number;318 max_importance: number;319 first_at: string;320 last_at: string;321 source?: SourceRef | null;322 events?: { id: string; slug: string; title: string; importance: number; event_type: string; detected_at: string; source_id: string; url: string; first_party?: boolean }[] | null;323 slug?: string | null;324 state?: string;325 source_count?: number;326 first_party_count?: number;327 external_count?: number;328 velocity?: number;329 lead_time_ms?: number | null;330 first_party_at?: string | null;331 first_external_at?: string | null;332 primary_slug?: string | null;333 event?: EventItem | null;334 sources?: { id: string; name: string; domain: string; first_party?: boolean }[] | null;335}336337export interface ClusterDetail {338 cluster: Cluster & { timeline?: { at: string; eventId: string; sourceId: string; sourceName?: string; sensorType?: string; firstParty: boolean; eventType: string; importance: number }[] };339 events: EventItem[];340 entities: { id: string; name: string; type: string; importance: number }[];341 propagation: { id: string; slug: string; at: string; offset_ms: number; source: SourceRef; sensor: SensorRef; first_party: boolean; event_type: string; importance: number; title: string }[];342 lead_time_ms: number | null;343 first_party_signals: number;344 external_signals: number;345}346347export interface BreakingDesk {348 breaking_now: Cluster[];349 developing: Cluster[];350 recently_confirmed: Cluster[];351 watching: EventItem[];352 generated_at: string;353}354355export interface Pulse {356 activity: { t: string; events: number; changes: number }[];357 desks: { desk: string; items: EventItem[] }[];358 rising_entities: { id: string; name: string; type: string; events_3h: number; events_prev_24h: number; acceleration: number }[];359 anomalies: { id: string; name: string; domain: string; changes_2h: number; baseline_per_day: number; activity_score: number; pct_vs_baseline: number | null }[];360 silent_changes: EventItem[];361 infrastructure: EventItem[];362 breaking: Cluster[];363 by_group_24h: Record<string, number>;364 totals: { events_1h: number; changes_1h: number; checks_5m: number; active_sources_24h: number; active_countries_24h: number };365 generated_at: string;366}367368export interface Radar {369 unusual_source_activity: { id: string; name: string; domain: string; categories: string[]; changes_3h: number; baseline_per_day: number; ratio: number }[];370 silent_clusters: { id: string; name: string; type: string; silent_24h: number; types: string[]; last_at: string }[];371 documentation_bursts: { id: string; name: string; domain: string; doc_changes_6h: number; types: string[]; last_at: string }[];372 repository_bursts: { id: string; name: string; domain: string; repo_events_6h: number; last_at: string }[];373 status_changes: EventItem[];374 developing: Cluster[];375 new_coverage: { id: string; name: string; source_id: string; source_name: string; first_event_at: string; events: number }[];376 generated_at: string;377 disclaimer: string;378}379380export interface EntityInsights {381 heatmap: { day: string; events: number; silent: number; breaking: number; max_importance: number }[];382 baseline_per_day: number;383 today: number;384 events_24h: number;385 events_prev_24h: number;386 velocity_ratio: number;387 anomaly: { score: number; ratio: number; pct: number };388 silent_24h: number;389 breaking_24h: number;390 sources_24h: number;391 most_active_sensors: { id: string; name: string; type: string; connector: string; source_id: string; events_7d: number; last_event_at: string }[];392 rank: { rank: number | null; total: number; score: number | null };393}394395export interface RankedEntity {396 id: string;397 name: string;398 type: string;399 domain?: string | null;400 importance: number;401 events_24h: number;402 events_7d: number;403 avg_signal: number;404 confirmed_ratio: number;405 sources: number;406 silent_24h: number;407 breaking_24h: number;408 last_at: string;409 baseline_per_day: number;410 rank_score: number;411 rank: number;412}413414export interface CountryRow {415 country: string;416 name: string;417 slug: string;418 flag: string;419 sources: number;420 events_24h: number;421 breaking_24h: number;422}423424export interface CountryDesk {425 country: { code: string; name: string; flag: string };426 breaking: EventItem[];427 by_category: { category: string; items: EventItem[] }[];428 sources: SourceRow[];429 by_type: { event_type: string; n: number }[];430 silent: EventItem[];431 recent: EventItem[];432 nextCursor: string | null;433}434435export interface CategoryDesk {436 channel: string;437 categories: string[];438 breaking: EventItem[];439 silent: EventItem[];440 active_sources: { id: string; name: string; domain: string; tier: string; first_party?: boolean; events_24h: number; max_importance: number }[];441 by_type: { event_type: string; n: number }[];442 trending_entities: { id: string; name: string; type: string; events_24h: number; sources: number }[];443 recent: EventItem[];444 nextCursor: string | null;445 series: { t: string; n: number }[];446}447448export interface Bookmark extends EventItem {449 bookmarked_at: string;450 note?: string | null;451}452453export interface Notification {454 id: number;455 alert_id: string;456 alert_name: string;457 event_id: string;458 channel: string;459 status: string;460 created_at: string;461 read_at?: string | null;462 event: { id: string; slug: string; title: string; importance: number; signal_score?: number | null; event_type: string; silent_change: boolean; detected_at: string; source: { id: string; name: string } };463}464465export interface Monitor {466 id: string;467 name: string;468 url: string;469 tier: string;470 health: string;471 status: string;472 enabled: boolean;473 config: Record<string, unknown>;474 next_check_at?: string | null;475 last_check_at?: string | null;476 last_change_at?: string | null;477 last_event_at?: string | null;478 last_status?: number | null;479 last_error?: string | null;480 total_runs?: number;481 raw_changes?: number;482 meaningful_changes?: number;483 created_at: string;484}485486export interface SourceRow {487 id: string;488 name: string;489 domain: string;490 homepage?: string | null;491 description?: string | null;492 categories: string[];493 tier: string;494 importance_weight?: number;495 enabled?: boolean;496 notes?: string | null;497 sensor_count?: number;498 event_count?: number;499 events_24h?: number;500 last_event_at?: string | null;501 last_check_at?: string | null;502 sensors_degraded?: number;503 robots_checked_at?: string | null;504 first_party?: boolean;505 country?: string | null;506 language?: string | null;507}508509export interface SensorRow {510 id: string;511 name: string;512 url: string;513 type: string;514 connector: string;515 tier: string;516 health: string;517 enabled: boolean;518 next_check_at?: string | null;519 last_check_at?: string | null;520 last_change_at?: string | null;521 last_event_at?: string | null;522 last_status?: number | null;523 last_error?: string | null;524 consecutive_errors?: number;525 total_runs?: number;526 total_not_modified?: number;527 raw_changes?: number;528 meaningful_changes?: number;529 avg_latency_ms?: number | null;530 base_interval_seconds?: number | null;531 has_etag?: boolean;532 has_last_modified?: boolean;533 source_id?: string;534 source_name?: string;535 domain?: string;536 config?: Record<string, unknown>;537 status?: string;538 priority?: number;539 validated_at?: string | null;540 etag?: string | null;541 last_modified?: string | null;542 checks_24h?: number;543 changes_24h?: number;544 snapshot_count?: number;545 current_interval_seconds?: number | null;546 not_modified_24h?: number;547 errors_24h?: number;548 avg_ms_24h?: number | null;549}550551export interface SourceQuality {552 sensors?: number;553 sensors_up?: number;554 avg_latency_ms?: number | null;555 raw_changes?: number;556 meaningful_changes?: number;557 total_runs?: number;558 total_not_modified?: number;559 structured_sensors?: number;560 checks_7d?: number;561 errors_7d?: number;562 avg_confidence?: number | null;563 success_rate: number;564 structured_share: number;565 usefulness: number;566 quality_score: number;567}568569export interface SourceDetail {570 source: SourceRow;571 sensors: SensorRow[];572 entities: { id: string; name: string; type: string; importance: number; event_count: number }[];573 activity: { changes_2h?: number; changes_14d?: number; events_24h?: number; events_14d?: number; baseline_changes_per_day?: number; activity_score?: number; silent_24h?: number; breaking_24h?: number };574 quality?: SourceQuality;575 discovery: { url: string; kind: string; evidence?: string | null; score?: { value?: number; itemCount?: number | null; title?: string | null } | null; status: string; found_at: string }[];576 by_type?: { event_type: string; n: number }[];577 daily?: { day: string; checks: number; not_modified: number; errors: number; raw_changes: number; events: number }[];578}579580export interface SensorDetail {581 sensor: SensorRow;582 runs: { id: string; started_at: string; finished_at?: string | null; http_status?: number | null; outcome: string; error?: string | null; duration_ms?: number | null; bytes?: number | null; fetch_method?: string | null; snapshot_id?: string | null }[];583 snapshots: SnapshotRow[];584 changes: ChangeRow[];585 events?: EventItem[];586}587588export interface SnapshotRow {589 id: string;590 url?: string;591 captured_at: string;592 http_status?: number | null;593 content_type?: string | null;594 content_length?: number | null;595 content_hash?: string;596 canonical_hash?: string;597 etag?: string | null;598 last_modified?: string | null;599 title?: string | null;600 mode?: string;601 fetch_duration_ms?: number | null;602 extraction_confidence?: number | null;603 storage_key?: string | null;604 has_raw?: boolean;605 has_change?: boolean;606 event_slug?: string | null;607}608609export interface ChangeRow {610 id: string;611 detected_at: string;612 kind: string;613 signal: number;614 noise_ratio?: number;615 magnitude?: number;616 meaningful: boolean;617 event_id?: string | null;618 old_snapshot_id?: string | null;619 new_snapshot_id?: string | null;620 heuristic_type?: string | null;621 diff?: DiffSummary;622 heuristic?: Record<string, unknown>;623 change_class?: string | null;624 field_changes?: FieldChange[] | null;625}626627export interface DiffSummary {628 kind: "text" | "json" | "list";629 added?: unknown[];630 removed?: unknown[];631 modified?: unknown[];632 changes?: { path: string; op: string; before?: unknown; after?: unknown }[];633 stats?: { added: number; removed: number; modified: number; unchangedRatio: number };634 counts?: { added: number; removed: number; modified: number };635 truncated?: boolean;636}637638export interface EventDetail {639 event: EventItem;640 related: EventItem[];641 cluster: Cluster | null;642 change: (ChangeRow & { diff: DiffSummary }) | null;643 interpretations: { version: number; model: string; created_at: string }[];644 snapshots: SnapshotRow[];645 sensor_reliability: { health?: string; success_rate?: number | null; avg_latency_ms?: number | null; total_runs?: number; raw_changes?: number; meaningful_changes?: number; last_check_at?: string | null } | null;646 history?: { id: string; slug: string; title: string; event_type: string; importance: number; silent_change: boolean; detected_at: string; field_changes?: FieldChange[] | null }[];647}648649export interface EntityRow {650 id: string;651 name: string;652 type: string;653 description?: string | null;654 domain?: string | null;655 homepage?: string | null;656 importance: number;657 categories: string[];658 parent_id?: string | null;659 event_count: number;660 last_event_at?: string | null;661 events_24h?: number;662}663664export interface EntityDetail {665 entity: EntityRow;666 parent?: { id: string; name: string; type: string } | null;667 children: EntityRow[];668 relations: { relation: string; from_id: string; to_id: string; from_name: string; to_name: string; to_type: string }[];669 sources: (SourceRef & { first_party?: boolean; country?: string | null; sensor_count?: number; events_24h?: number })[];670 aliases: string[];671 recent: EventItem[];672 nextCursor?: string | null;673 by_type: { event_type: string; n: number }[];674 insights?: EntityInsights;675 silent?: EventItem[];676 related?: { id: string; name: string; type: string; shared_events: number }[];677}678679export interface Explore {680 most_active_sources: { id: string; name: string; domain: string; first_party?: boolean; events_24h: number; max_importance: number }[];681 biggest_changes: EventItem[];682 silent_changes: EventItem[];683 clusters: Cluster[];684 unusual_activity: { id: string; name: string; domain: string; changes_2h: number; baseline_per_day: number; activity_score: number; pct_vs_baseline?: number | null }[];685 newly_detected?: EventItem[];686 confirmed_first_party?: EventItem[];687 by_type: { event_type: string; n: number }[];688 by_category: { category: string; n: number }[];689 channels: Record<string, string[]>;690 groups?: Record<string, string>;691 event_types: Record<string, string>;692}693694export interface ConnectorHealthRow {695 connector: string;696 status: string;697 runs_24h: number;698 errors_24h: number;699 success_rate?: number | null;700 avg_latency_ms?: number | null;701 changes_24h: number;702 events_24h: number;703 last_success_at?: string | null;704 last_error_at?: string | null;705 last_error?: string | null;706 http_codes: Record<string, number>;707 rate_limit_until?: string | null;708 updated_at: string;709}710711export interface HealthReport {712 connectors: ConnectorHealthRow[];713 sensors_by_health: { health: string; n: number }[];714 degraded_sensors: SensorRow[];715 noisy_sensors: { id: string; name: string; source_id: string; url: string; raw_changes: number; meaningful_changes: number; noise_ratio: number | null }[];716 daily: Record<string, number | string>[];717 live: { clients: number; published: number };718 sensors_by_status?: { status: string; n: number }[];719 sensors_by_connector?: { connector: string; sensors: number; up: number; avg_latency_ms: number | null }[];720 throughput?: { checks_5m: number; not_modified_5m: number; events_5m: number; changes_5m: number; queue_due: number; avg_latency_5m_ms: number | null; noise_filtered_24h: number; checks_per_min: number; events_per_min: number; not_modified_ratio: number | null };721 engine?: { inflight?: number; concurrency?: number; due?: number; busyHosts?: { host: string; inflight: number }[]; circuitOpen?: { host: string; failures: number; until: string }[]; at?: string; version?: string } | null;722 top_failing_domains?: { host: string; failures: number; last_error: string | null }[];723 slowest_sensors?: { id: string; name: string; source_id: string; avg_latency_ms: number; connector: string }[];724}725726export interface SearchResult {727 query: string;728 parsed?: { text: string; filters: Record<string, unknown> };729 events: EventItem[];730 entities: EntityRow[];731 sources: SourceRow[];732 urls: { url: string; domain: string; status: string; change_count: number; last_seen_at: string }[];733 clusters?: { id: string; slug: string; title: string; state: string; event_count: number; source_count: number; max_importance: number; last_at: string }[];734}735736export interface DomainTimeline {737 domain: string;738 source_id: string | null;739 urls: { url: string; status: string; first_seen_at: string; last_seen_at: string; snapshot_count: number; change_count: number; sensor_id?: string | null }[];740 events: EventItem[];741 nextCursor: string | null;742}743744export interface UrlHistory {745 url: { url: string; domain?: string; status?: string; first_seen_at?: string; last_seen_at?: string; snapshot_count?: number; change_count?: number; sensor_id?: string | null };746 history: { id: number; at: string; kind: string; snapshot_id?: string | null; change_id?: string | null; event_id?: string | null; note?: string | null; event_title?: string | null; importance?: number | null; event_type?: string | null; event_slug?: string | null; change_kind?: string | null; signal?: number | null }[];747 snapshots: SnapshotRow[];748}749750export interface CompareResult {751 a: SnapshotRow;752 b: SnapshotRow;753 before: string;754 after: string;755 diff: { unified: string; stats: { added: number; removed: number; modified: number; unchangedRatio: number }; added: string[]; removed: string[]; modified: { before: string; after: string }[] };756}757758export interface Watchlist {759 id: string;760 name: string;761 owner_token?: string;762 created_at: string;763 items: { kind: string; value: string; added_at?: string }[];764}765766export interface AlertRule {767 importance_min?: number;768 signal_min?: number;769 event_types?: string[];770 groups?: string[];771 entities?: string[];772 sources?: string[];773 keywords?: string[];774 silent_only?: boolean;775 first_party_only?: boolean;776 confirmed_only?: boolean;777 categories?: string[];778 countries?: string[];779}780781export interface Alert {782 id: string;783 name: string;784 rule: AlertRule;785 channel: string;786 channel_config?: { url?: string };787 enabled: boolean;788 created_at: string;789 last_fired_at?: string | null;790 fired_count?: number;791 fired_24h?: number;792}793794// ---------------------------------------------------------------------------------------795// Server-side fetch helpers (never throw)796// ---------------------------------------------------------------------------------------797798async function getJson<T>(path: string, fallback: T, init?: RequestInit & { revalidate?: number }): Promise<T> {799 try {800 const url = `${serverApiBase()}${path}`;801 const res = await fetch(url, { ...(init?.revalidate ? { next: { revalidate: init.revalidate } } : { cache: "no-store" }), headers: { accept: "application/json" }, signal: AbortSignal.timeout(8000) });802 if (!res.ok) return fallback;803 return (await res.json()) as T;804 } catch {805 return fallback;806 }807}808809export type EventQuery = Partial<{810 after: string;811 before: string;812 category: string;813 entity: string;814 source: string;815 domain: string;816 sensor: string;817 cluster: string;818 importance_min: number;819 confidence_min: number;820 signal_min: number;821 event_type: string;822 group: string;823 silent_change: boolean;824 first_party: boolean;825 confirmed: boolean;826 country: string;827 language: string;828 change_class: string;829 q: string;830 limit: number;831 cursor: string;832 order: "recent" | "importance" | "signal";833}>;834835export function eventQueryString(q: EventQuery): string {836 const p = new URLSearchParams();837 for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== "") p.set(k, String(v));838 const s = p.toString();839 return s ? `?${s}` : "";840}841842export const api = {843 events: (q: EventQuery = {}) => getJson<EventsPage>(`/api/v1/events${eventQueryString(q)}`, { items: [], nextCursor: null }),844 /** Signal ≥ 80 over the last 48 h, ranked (legacy list; the /breaking desk uses `breakingDesk`). */845 breaking: (limit = 60) => getJson<EventsPage>(`/api/v1/events${eventQueryString({ signal_min: 80, order: "signal", after: new Date(Date.now() - 48 * 3600e3).toISOString(), limit })}`, { items: [], nextCursor: null }),846 breakingDesk: () => getJson<BreakingDesk | null>("/api/v1/breaking", null),847 pulse: () => getJson<Pulse | null>("/api/v1/pulse", null),848 radar: () => getJson<Radar | null>("/api/v1/radar", null),849 cluster: (idOrSlug: string) => getJson<ClusterDetail | null>(`/api/v1/clusters/${encodeURIComponent(idOrSlug)}`, null),850 rank: (limit = 100) => getJson<{ items: RankedEntity[] }>(`/api/v1/entities/rank?limit=${limit}`, { items: [] }),851 countries: () => getJson<{ items: CountryRow[] }>("/api/v1/countries", { items: [] }),852 country: (code: string) => getJson<CountryDesk | null>(`/api/v1/countries/${encodeURIComponent(code)}`, null),853 categoryDesk: (channel: string) => getJson<CategoryDesk | null>(`/api/v1/categories/${encodeURIComponent(channel)}`, null),854 sensorSnapshots: (id: string, limit = 200) => getJson<{ items: SnapshotRow[] }>(`/api/v1/sensors/${encodeURIComponent(id)}/snapshots?limit=${limit}`, { items: [] }),855 eventCount: (q: EventQuery = {}) => getJson<{ count: number }>(`/api/v1/events/count${eventQueryString(q)}`, { count: 0 }),856 event: (idOrSlug: string) => getJson<EventDetail | null>(`/api/v1/events/${encodeURIComponent(idOrSlug)}`, null),857 stats: () => getJson<Stats>("/api/v1/stats", {}, { revalidate: 5 }),858 trending: (hours = 24, limit = 10) => getJson<{ items: TrendingItem[] }>(`/api/v1/trending?hours=${hours}&limit=${limit}`, { items: [] }),859 explore: () => getJson<Explore | null>("/api/v1/explore", null),860 clusters: (limit = 20, since = 72) => getJson<{ items: Cluster[] }>(`/api/v1/clusters?limit=${limit}&since=${since}`, { items: [] }),861 sources: (q: { category?: string; q?: string; country?: string; tier?: string; first_party?: string } = {}) => getJson<{ items: SourceRow[] }>(`/api/v1/sources${eventQueryString(q as EventQuery)}`, { items: [] }),862 source: (id: string) => getJson<SourceDetail | null>(`/api/v1/sources/${encodeURIComponent(id)}`, null),863 sensor: (id: string) => getJson<SensorDetail | null>(`/api/v1/sensors/${encodeURIComponent(id)}`, null),864 entities: (q: { type?: string; q?: string; limit?: number } = {}) => getJson<{ items: EntityRow[] }>(`/api/v1/entities${eventQueryString(q)}`, { items: [] }),865 entity: (id: string) => getJson<EntityDetail | null>(`/api/v1/entities/${encodeURIComponent(id)}`, null),866 entityTimeline: (id: string, cursor?: string, limit = 100) => getJson<EventsPage>(`/api/v1/entities/${encodeURIComponent(id)}/timeline${eventQueryString({ cursor, limit })}`, { items: [], nextCursor: null }),867 domain: (domain: string, cursor?: string) => getJson<DomainTimeline | null>(`/api/v1/domains/${encodeURIComponent(domain)}/timeline${eventQueryString({ cursor })}`, null),868 urlHistory: (url: string) => getJson<UrlHistory | null>(`/api/v1/urls/history?url=${encodeURIComponent(url)}`, null),869 compare: (a: string, b: string) => getJson<CompareResult | null>(`/api/v1/snapshots/compare?a=${encodeURIComponent(a)}&b=${encodeURIComponent(b)}`, null),870 change: (id: string) => getJson<{ change: ChangeRow & { diff: DiffSummary; sensor_url?: string; sensor_name?: string; source_id?: string }; unified: string | null } | null>(`/api/v1/changes/${encodeURIComponent(id)}`, null),871 health: () => getJson<HealthReport | null>("/api/v1/health/connectors", null),872 coverage: () => getJson<CoverageSummary | null>("/api/v1/coverage", null, { revalidate: 60 }),873 coverageSector: (sector: string) => getJson<CoverageSectorDetail | null>(`/api/v1/coverage/${encodeURIComponent(sector)}`, null, { revalidate: 60 }),874 search: (q: string) => getJson<SearchResult>(`/api/v1/search?q=${encodeURIComponent(q)}`, { query: q, events: [], entities: [], sources: [], urls: [] }),875};876