From daf5f43f01dde8699cf2c601ff55ea0079c099e8 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Wed, 26 Aug 2026 15:41:10 +0000 Subject: [PATCH] =?UTF-8?q?feat(FE):=20Intelligence=20Hub=20page=20?= =?UTF-8?q?=E2=80=94=20ClickHouse=20status,=20safe=20queries,=20legislatio?= =?UTF-8?q?n=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/dashboard/intelligence/page.tsx | 356 +++++++++--------------- 1 file changed, 132 insertions(+), 224 deletions(-) diff --git a/src/app/dashboard/intelligence/page.tsx b/src/app/dashboard/intelligence/page.tsx index a4849ef..44c9f70 100644 --- a/src/app/dashboard/intelligence/page.tsx +++ b/src/app/dashboard/intelligence/page.tsx @@ -1,252 +1,160 @@ 'use client'; -import Link from 'next/link'; -import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { useQuery, useMutation } from '@tanstack/react-query'; import { apiFetch } from '../../../lib/api'; -import type { SavedSegment, ResearchBrief, FinancialSignal } from '../../../lib/api'; import { useSession } from '../../../components/session-provider'; -const B = '/dashboard'; +interface ChResult { data: Record[]; rows: number; elapsed: number; error?: string; } +interface ChStatus { ok: boolean; version?: string; error?: string; } -const SEV_CFG = { - critical: { borderClass: 'border-l-signal-danger', badgeClass: 'bg-signal-danger text-white', icon: '!' }, - warning: { borderClass: 'border-l-signal-warn', badgeClass: 'bg-signal-warn text-white', icon: '⚠' }, - info: { borderClass: 'border-l-bronze-deep', badgeClass: 'bg-bronze-wash text-bronze-deep', icon: 'i' }, -} as const; +const SAFE_QUERIES = [ + { key: 'databases', label: 'Databases disponibile', params: {} }, + { key: 'tables', label: 'Tabele (toate)', params: {} }, +]; -const CAT_LABEL: Record = { - macro: 'Macro', fx: 'Valute', rates: 'Rate', news: 'Știri', commodity: 'Materii prime', -}; - -export default function IntelligenceOverviewPage() { +export default function IntelligenceHubPage() { const { activeTenant } = useSession(); const tenantId = activeTenant?.tenantId ?? ''; - const opts = { enabled: Boolean(tenantId), staleTime: 60_000 }; + const [legislationQ, setLegislationQ] = useState(''); + const [result, setResult] = useState(null); + const [activeQuery, setActiveQuery] = useState(''); - const { data: segments = [], isLoading: loadingSegs } = useQuery({ - queryKey: ['segments-intel', tenantId], - queryFn: () => apiFetch('/v1/segments', { tenantId }), - ...opts, + const { data: status, isLoading: loadStatus } = useQuery({ + queryKey: ['ch-status', tenantId], + queryFn: () => apiFetch('/v1/intelligence/status', { tenantId }), + enabled: Boolean(tenantId), staleTime: 30_000, }); - const { data: briefs = [], isLoading: loadingBriefs } = useQuery({ - queryKey: ['briefs-intel', tenantId], - queryFn: () => apiFetch('/v1/research-briefs', { tenantId }), - ...opts, + const runQueryMut = useMutation({ + mutationFn: (queryKey: string) => + apiFetch('/v1/intelligence/query', { + tenantId, method: 'POST', body: { queryKey }, + }), + onSuccess: (data, key) => { setResult(data); setActiveQuery(key); }, }); - const { data: signals = [], isLoading: loadingSignals } = useQuery({ - queryKey: ['financial-signals', tenantId], - queryFn: () => apiFetch('/v1/financial-intelligence/signals?limit=20', { tenantId }), - ...opts, - staleTime: 300_000, + const searchMut = useMutation({ + mutationFn: (q: string) => + apiFetch(`/v1/intelligence/legislation?q=${encodeURIComponent(q)}`, { tenantId }), + onSuccess: (data) => { setResult(data); setActiveQuery('legislation'); }, }); - const isLoading = loadingSegs || loadingBriefs || loadingSignals; - - const recentBriefs = [...briefs].sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ).slice(0, 4); - - const criticalSignals = signals.filter((s) => s.severity === 'critical'); - const warningSignals = signals.filter((s) => s.severity === 'warning'); - const lastSignalAt = signals[0]?.publishedAt; + const chOk = status?.ok; return ( -
+
-

Intelligence Overview

-

- Motorul de intelligence B2B + semnale de piață financiară internațională. +

Intelligence Hub

+

ClickHouse · {status?.version ?? '…'} · 147M+ rânduri

+
+ + {/* Status card */} +
+ {chOk ? '🟢' : '🔴'} +
+

+ ClickHouse {chOk ? 'conectat' : 'indisponibil'} +

+

+ {status?.error ?? `supersrv:8123 · DB: ${process.env.NEXT_PUBLIC_CH_DB ?? 'default'}`} +

+
+ {!chOk && ( +
+

Setează în Coolify:

+ CLICKHOUSE_URL, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD +
+ )} +
+ + {/* Quick queries */} +
+

Query-uri predefinite (safe)

+
+ {SAFE_QUERIES.map((q) => ( + + ))} +
+
+ + {/* Legislation search */} +
+

Căutare legislație DE + ESCO

+
+ setLegislationQ(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && legislationQ.trim() && searchMut.mutate(legislationQ)} + className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + +
+

+ Caută în tabelul default.legislation din ClickHouse. + Dacă tabela nu există — importă datele cu Data Ingest Toolkit.

- {isLoading ? ( -

Se încarcă…

- ) : ( - <> - {/* ── MARKET SIGNALS ─────────────────────────────────────────── */} -
-
-

- Market Intelligence - {(criticalSignals.length > 0 || warningSignals.length > 0) && ( - - {criticalSignals.length + warningSignals.length} alerte - - )} -

- {lastSignalAt && ( - - Actualizat: {new Date(lastSignalAt).toLocaleString('ro-RO', { - month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', - })} - + {/* Results */} + {result && ( +
+
+

+ Rezultate — {result.rows} rânduri în {result.elapsed}ms +

+ +
+ {result.error ? ( +

{result.error}

+ ) : result.data.length === 0 ? ( +

Niciun rezultat.

+ ) : ( +
+ + + + {Object.keys(result.data[0]).map((k) => ( + + ))} + + + + {result.data.slice(0, 50).map((row, i) => ( + + {Object.values(row).map((v, j) => ( + + ))} + + ))} + +
{k}
{String(v)}
+ {result.data.length > 50 && ( +

+{result.data.length - 50} rânduri ascunse

)}
- - {signals.length === 0 ? ( -
-

Niciun semnal de piață disponibil

-

- Semnalele sunt generate automat de n8n din World Bank, IMF și Eurostat. -

-

- Importați workflow-ul n8n din /dashboard/data sau contactați administratorul. -

-
- ) : ( -
- {signals.slice(0, 8).map((sig) => { - const cfg = SEV_CFG[sig.severity as keyof typeof SEV_CFG] ?? SEV_CFG.info; - return ( -
-
-
-
- - {cfg.icon} - - - {CAT_LABEL[sig.category] ?? sig.category} - - - {sig.region} - - {sig.niche && ( - - {sig.niche} - - )} -
-

- {sig.title} -

- {sig.rawValue != null && ( -

- {Number(sig.rawValue).toFixed(2)}{sig.unit ? ` ${sig.unit}` : ''} - {sig.changePercent != null && ( - = 0 ? 'text-signal-ok ml-1' : 'text-signal-danger ml-1'}> - ({Number(sig.changePercent) >= 0 ? '+' : ''}{Number(sig.changePercent).toFixed(2)}%) - - )} -

- )} -

{sig.summary}

-
-
-

{sig.source}

-

- {new Date(sig.publishedAt).toLocaleDateString('ro-RO')} -

-
-
-
- ); - })} - {signals.length > 8 && ( -
- +{signals.length - 8} semnale suplimentare disponibile -
- )} -
- )} -
- - {/* ── B2B INTELLIGENCE STATS ─────────────────────────────────── */} -
- {[ - { label: 'Segmente salvate', value: segments.length, href: `${B}/segments` }, - { label: 'Research Briefs', value: briefs.length, href: `${B}/research` }, - { label: 'Companii B2B', value: '147M+', href: `${B}/companies` }, - { label: 'Semnale piață', value: signals.length, href: '#' }, - ].map((s) => ( - -

{s.label}

-

{s.value}

- - ))} -
- - {/* ── B2B MODULES ────────────────────────────────────────────── */} -
-
-
-

Research recente

- Toate → -
- {recentBriefs.length === 0 ? ( -

Niciun brief creat.

- ) : ( -
    - {recentBriefs.map((b) => ( -
  • -

    {b.title}

    -
    - {b.status} - {new Date(b.createdAt).toLocaleDateString('ro-RO')} -
    -
  • - ))} -
- )} -
- -
-
-

Segmente active

- Gestionează → -
- {segments.length === 0 ? ( -

Niciun segment salvat.

- ) : ( -
    - {segments.slice(0, 6).map((s) => ( -
  • -

    {s.name}

    - - limit {s.resultLimit} - -
  • - ))} - {segments.length > 6 && ( -
  • +{segments.length - 6} mai multe
  • - )} -
- )} -
- - {/* Capability map */} -
-

- Capabilități Intelligence -

-
- {[ - { label: 'Company Search', status: 'live', href: `${B}/companies` }, - { label: 'Saved Segments', status: 'live', href: `${B}/segments` }, - { label: 'Research Briefs', status: 'live', href: `${B}/research` }, - { label: 'Market Signals', status: signals.length > 0 ? 'live' : 'setup', href: '#' }, - { label: 'People Intelligence', status: 'a3', href: '#' }, - { label: 'Market Intelligence', status: 'a4', href: '#' }, - { label: 'Insights Engine', status: 'a3', href: '#' }, - { label: 'Digital Twin', status: 'a4', href: '#' }, - ].map((cap) => ( -
-

{cap.label}

- - {cap.status === 'live' ? '● live' : cap.status === 'setup' ? '○ setup' : cap.status} - -
- ))} -
-
-
- + )} +
)} + + {/* Architecture note */} +
+

Arhitectură conexiune C2

+ + ceo-web → /v1/intelligence/* → ceo-api IntelligenceService → ClickHouse HTTP :8123 (supersrv) + +

+ Variabile necesare în Coolify → ceo-api env: CLICKHOUSE_URL · CLICKHOUSE_USER · CLICKHOUSE_PASSWORD · CLICKHOUSE_DATABASE +

+
); }