phase 16: React dashboard with full platform control and analytics
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* Typed TanStack Query hooks for all API domains.
|
||||
* Requirements: 13.1, 13.2
|
||||
*/
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPut } from './client';
|
||||
import type { ApiBase } from './client';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function useGet<T>(key: unknown[], api: ApiBase, path: string, enabled = true) {
|
||||
return useQuery<T>({ queryKey: key, queryFn: () => apiGet<T>(api, path), enabled });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Companies (Query API)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Company {
|
||||
id: string;
|
||||
ticker: string;
|
||||
legal_name: string;
|
||||
exchange: string | null;
|
||||
sector: string | null;
|
||||
industry: string | null;
|
||||
market_cap_bucket: string | null;
|
||||
active: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
aliases?: Alias[];
|
||||
active_source_count?: number;
|
||||
}
|
||||
|
||||
export interface Alias {
|
||||
id: string;
|
||||
alias: string;
|
||||
alias_type: string;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
id: string;
|
||||
source_type: string;
|
||||
source_name: string;
|
||||
config?: Record<string, unknown>;
|
||||
credibility_score: number;
|
||||
retention_days?: number;
|
||||
access_policy?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export function useCompanies(params?: { active?: boolean; sector?: string; ticker?: string }) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.active !== undefined) qs.set('active', String(params.active));
|
||||
if (params?.sector) qs.set('sector', params.sector);
|
||||
if (params?.ticker) qs.set('ticker', params.ticker);
|
||||
const path = `/api/companies${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<Company[]>(['companies', params], 'query', path);
|
||||
}
|
||||
|
||||
export function useCompany(id: string | undefined) {
|
||||
return useGet<Company>(['company', id], 'query', `/api/companies/${id}`, !!id);
|
||||
}
|
||||
|
||||
export function useCompanySources(companyId: string | undefined) {
|
||||
return useGet<Source[]>(['company-sources', companyId], 'query', `/api/companies/${companyId}/sources`, !!companyId);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Symbol Registry (CRUD)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useRegistryCompanies(active = true) {
|
||||
return useGet<Company[]>(['registry-companies', active], 'registry', `/companies?active=${active}`);
|
||||
}
|
||||
|
||||
export function useCreateCompany() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { ticker: string; legal_name: string; sector?: string; industry?: string; exchange?: string; market_cap_bucket?: string }) =>
|
||||
apiPost<Company>('registry', '/companies', body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['companies'] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateSource(companyId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { source_type: string; source_name: string; config?: Record<string, unknown>; credibility_score?: number; retention_days?: number; access_policy?: string }) =>
|
||||
apiPost<Source>('registry', `/companies/${companyId}/sources`, body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['company-sources', companyId] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateAlias(companyId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { alias: string; alias_type?: string }) =>
|
||||
apiPost<Alias>('registry', `/companies/${companyId}/aliases`, body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['company', companyId] }),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Watchlists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Watchlist {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export function useWatchlists() {
|
||||
return useGet<Watchlist[]>(['watchlists'], 'registry', '/watchlists');
|
||||
}
|
||||
|
||||
export function useCreateWatchlist() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { name: string; description?: string }) =>
|
||||
apiPost<Watchlist>('registry', '/watchlists', body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['watchlists'] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useWatchlistMembers(watchlistId: string | undefined) {
|
||||
return useGet<Company[]>(['watchlist-members', watchlistId], 'registry', `/watchlists/${watchlistId}/members`, !!watchlistId);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Documents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Document {
|
||||
id: string;
|
||||
document_type: string;
|
||||
source_type: string;
|
||||
publisher: string | null;
|
||||
url: string | null;
|
||||
title: string | null;
|
||||
published_at: string | null;
|
||||
retrieved_at: string | null;
|
||||
language: string | null;
|
||||
content_hash: string | null;
|
||||
parse_quality_score: number | null;
|
||||
parse_confidence: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface DocumentDetail extends Document {
|
||||
canonical_url: string | null;
|
||||
raw_storage_ref: string | null;
|
||||
normalized_storage_ref: string | null;
|
||||
company_mentions: Array<{ company_id: string; ticker: string; mention_type: string; confidence: number; legal_name: string }>;
|
||||
intelligence: DocumentIntelligence | null;
|
||||
}
|
||||
|
||||
export interface DocumentIntelligence {
|
||||
id: string;
|
||||
summary: string | null;
|
||||
macro_themes: string[] | null;
|
||||
novelty_score: number | null;
|
||||
source_credibility: number | null;
|
||||
extraction_warnings: string[] | null;
|
||||
confidence: number | null;
|
||||
model_provider: string | null;
|
||||
model_name: string | null;
|
||||
prompt_version: string | null;
|
||||
schema_version: string | null;
|
||||
validation_status: string | null;
|
||||
company_impacts?: CompanyImpact[];
|
||||
}
|
||||
|
||||
export interface CompanyImpact {
|
||||
company_id: string;
|
||||
ticker: string;
|
||||
legal_name: string;
|
||||
relevance: number;
|
||||
sentiment: string;
|
||||
impact_score: number;
|
||||
impact_horizon: string;
|
||||
catalyst_type: string;
|
||||
key_facts: string[] | null;
|
||||
risks: string[] | null;
|
||||
evidence_spans: string[] | null;
|
||||
}
|
||||
|
||||
export function useDocuments(params?: { ticker?: string; document_type?: string; status?: string; since?: string; limit?: number; offset?: number }) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.ticker) qs.set('ticker', params.ticker);
|
||||
if (params?.document_type) qs.set('document_type', params.document_type);
|
||||
if (params?.status) qs.set('status', params.status);
|
||||
if (params?.since) qs.set('since', params.since);
|
||||
if (params?.limit) qs.set('limit', String(params.limit));
|
||||
if (params?.offset) qs.set('offset', String(params.offset));
|
||||
const path = `/api/documents${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<Document[]>(['documents', params], 'query', path);
|
||||
}
|
||||
|
||||
export function useDocument(id: string | undefined) {
|
||||
return useGet<DocumentDetail>(['document', id], 'query', `/api/documents/${id}`, !!id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trends
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface TrendSummary {
|
||||
id: string;
|
||||
entity_type: string;
|
||||
entity_id: string;
|
||||
window: string;
|
||||
trend_direction: string;
|
||||
trend_strength: number;
|
||||
confidence: number;
|
||||
top_supporting_evidence: string[] | null;
|
||||
top_opposing_evidence: string[] | null;
|
||||
dominant_catalysts: string[] | null;
|
||||
material_risks: string[] | null;
|
||||
contradiction_score: number;
|
||||
market_context: Record<string, unknown> | null;
|
||||
generated_at: string;
|
||||
}
|
||||
|
||||
export function useTrends(params?: { ticker?: string; window?: string; limit?: number; offset?: number }) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.ticker) qs.set('ticker', params.ticker);
|
||||
if (params?.window) qs.set('window', params.window);
|
||||
if (params?.limit) qs.set('limit', String(params.limit));
|
||||
if (params?.offset) qs.set('offset', String(params.offset));
|
||||
const path = `/api/trends${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<TrendSummary[]>(['trends', params], 'query', path);
|
||||
}
|
||||
|
||||
export function useTrend(id: string | undefined) {
|
||||
return useGet<TrendSummary>(['trend', id], 'query', `/api/trends/${id}`, !!id);
|
||||
}
|
||||
|
||||
export function useTrendEvidence(id: string | undefined) {
|
||||
return useGet<{ trend: TrendSummary; evidence: unknown[] }>(['trend-evidence', id], 'query', `/api/trends/${id}/evidence`, !!id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Recommendations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Recommendation {
|
||||
id: string;
|
||||
ticker: string;
|
||||
action: string;
|
||||
mode: string;
|
||||
confidence: number;
|
||||
time_horizon: string;
|
||||
thesis: string | null;
|
||||
invalidation_conditions: string[] | null;
|
||||
portfolio_pct: number | null;
|
||||
max_loss_pct: number | null;
|
||||
model_version: string | null;
|
||||
risk_classification: string | null;
|
||||
generated_at: string;
|
||||
}
|
||||
|
||||
export interface RecommendationDetail extends Recommendation {
|
||||
company_id: string | null;
|
||||
evidence: Array<{ id: string; document_id: string; intelligence_id: string; evidence_type: string; weight: number; title: string; document_type: string; source_type: string; publisher: string; url: string; published_at: string }>;
|
||||
risk_evaluation: { id: string; eligible: boolean; allowed_mode: string; rejection_reasons: string[] | null; risk_checks: Record<string, unknown> | null; evaluated_at: string } | null;
|
||||
}
|
||||
|
||||
export function useRecommendations(params?: { ticker?: string; action?: string; mode?: string; since?: string; limit?: number; offset?: number }) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.ticker) qs.set('ticker', params.ticker);
|
||||
if (params?.action) qs.set('action', params.action);
|
||||
if (params?.mode) qs.set('mode', params.mode);
|
||||
if (params?.since) qs.set('since', params.since);
|
||||
if (params?.limit) qs.set('limit', String(params.limit));
|
||||
if (params?.offset) qs.set('offset', String(params.offset));
|
||||
const path = `/api/recommendations${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<Recommendation[]>(['recommendations', params], 'query', path);
|
||||
}
|
||||
|
||||
export function useRecommendation(id: string | undefined) {
|
||||
return useGet<RecommendationDetail>(['recommendation', id], 'query', `/api/recommendations/${id}`, !!id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
recommendation_id: string | null;
|
||||
broker_account_id: string | null;
|
||||
ticker: string;
|
||||
side: string;
|
||||
order_type: string;
|
||||
quantity: number;
|
||||
limit_price: number | null;
|
||||
stop_price: number | null;
|
||||
status: string;
|
||||
broker_order_id: string | null;
|
||||
submitted_at: string | null;
|
||||
fill_price: number | null;
|
||||
fill_quantity: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface OrderDetail extends Order {
|
||||
idempotency_key: string | null;
|
||||
decision_trace: Record<string, unknown> | null;
|
||||
events: Array<{ id: string; event_type: string; data: unknown; broker_timestamp: string | null; created_at: string }>;
|
||||
audit_trail: unknown[];
|
||||
}
|
||||
|
||||
export function useOrders(params?: { ticker?: string; status?: string; side?: string; since?: string; limit?: number; offset?: number }) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.ticker) qs.set('ticker', params.ticker);
|
||||
if (params?.status) qs.set('status', params.status);
|
||||
if (params?.side) qs.set('side', params.side);
|
||||
if (params?.since) qs.set('since', params.since);
|
||||
if (params?.limit) qs.set('limit', String(params.limit));
|
||||
if (params?.offset) qs.set('offset', String(params.offset));
|
||||
const path = `/api/orders${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<Order[]>(['orders', params], 'query', path);
|
||||
}
|
||||
|
||||
export function useOrder(id: string | undefined) {
|
||||
return useGet<OrderDetail>(['order', id], 'query', `/api/orders/${id}`, !!id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Positions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Position {
|
||||
id: string;
|
||||
broker_account_id: string | null;
|
||||
ticker: string;
|
||||
quantity: number;
|
||||
avg_entry_price: number;
|
||||
current_price: number | null;
|
||||
unrealized_pnl: number | null;
|
||||
realized_pnl: number | null;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export function usePositions(ticker?: string) {
|
||||
const path = ticker ? `/api/positions?ticker=${ticker}` : '/api/positions';
|
||||
return useGet<Position[]>(['positions', ticker], 'query', path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin: Trading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface TradingConfig {
|
||||
id?: string;
|
||||
name?: string;
|
||||
trading_mode: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface Approval {
|
||||
id: string;
|
||||
order_job: unknown;
|
||||
recommendation_id: string | null;
|
||||
ticker: string;
|
||||
side: string;
|
||||
quantity: number;
|
||||
estimated_value: number | null;
|
||||
status: string;
|
||||
requested_at: string;
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
export interface Lockout {
|
||||
id: string;
|
||||
ticker: string;
|
||||
lockout_type: string;
|
||||
reason: string;
|
||||
expires_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function useTradingConfig() {
|
||||
return useGet<TradingConfig>(['trading-config'], 'query', '/api/admin/trading/config');
|
||||
}
|
||||
|
||||
export function useSetTradingMode() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (mode: string) => apiPut<unknown>('query', `/api/admin/trading/mode?mode=${mode}`, {}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['trading-config'] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePendingApprovals() {
|
||||
return useGet<Approval[]>(['pending-approvals'], 'query', '/api/admin/trading/approvals');
|
||||
}
|
||||
|
||||
export function useReviewApproval() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, approved, review_note }: { id: string; approved: boolean; review_note?: string }) =>
|
||||
apiPut<unknown>('query', `/api/admin/trading/approvals/${id}?approved=${approved}&reviewed_by=operator&review_note=${encodeURIComponent(review_note ?? '')}`, {}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['pending-approvals'] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useActiveLockouts() {
|
||||
return useGet<Lockout[]>(['lockouts'], 'query', '/api/admin/trading/lockouts');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin: Sources
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface SourceHealth {
|
||||
source_id: string;
|
||||
source_type: string;
|
||||
source_name: string;
|
||||
credibility_score: number;
|
||||
active: boolean;
|
||||
ticker: string;
|
||||
legal_name: string;
|
||||
company_id: string;
|
||||
last_run_status: string | null;
|
||||
last_run_at: string | null;
|
||||
last_error: string | null;
|
||||
total_runs_24h: number;
|
||||
failed_runs_24h: number;
|
||||
total_items_24h: number;
|
||||
}
|
||||
|
||||
export function useSourceHealth(params?: { source_type?: string; company_id?: string }) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.source_type) qs.set('source_type', params.source_type);
|
||||
if (params?.company_id) qs.set('company_id', params.company_id);
|
||||
const path = `/api/admin/sources/health${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<SourceHealth[]>(['source-health', params], 'query', path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ops: Pipeline, Ingestion, Model, Coverage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function usePipelineHealth(hours = 24) {
|
||||
return useGet<Record<string, unknown>>(['pipeline-health', hours], 'query', `/api/ops/pipeline/health?hours=${hours}`);
|
||||
}
|
||||
|
||||
export function useIngestionSummary(hours = 24) {
|
||||
return useGet<Record<string, unknown>>(['ingestion-summary', hours], 'query', `/api/ops/ingestion/summary?hours=${hours}`);
|
||||
}
|
||||
|
||||
export function useIngestionThroughput(hours = 24, bucket = '1h') {
|
||||
return useGet<unknown[]>(['ingestion-throughput', hours, bucket], 'query', `/api/ops/ingestion/throughput?hours=${hours}&bucket=${bucket}`);
|
||||
}
|
||||
|
||||
export function useModelPerformance(hours = 24) {
|
||||
return useGet<Record<string, unknown>>(['model-performance', hours], 'query', `/api/ops/model/performance?hours=${hours}`);
|
||||
}
|
||||
|
||||
export function useModelFailures(hours = 24) {
|
||||
return useGet<unknown[]>(['model-failures', hours], 'query', `/api/ops/model/failures?hours=${hours}`);
|
||||
}
|
||||
|
||||
export function useCoverageGaps() {
|
||||
return useGet<{ missing_source_types: unknown[]; stale_sources: unknown[] }>(['coverage-gaps'], 'query', '/api/ops/sources/coverage-gaps');
|
||||
}
|
||||
|
||||
export function useSymbolCoverage() {
|
||||
return useGet<unknown[]>(['symbol-coverage'], 'query', '/api/admin/companies/coverage');
|
||||
}
|
||||
Reference in New Issue
Block a user