@neat.is/web 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import Link from 'next/link'
5
+
6
+ interface Incident {
7
+ nodeId: string
8
+ timestamp: string
9
+ type: string
10
+ message: string
11
+ stacktrace?: string
12
+ }
13
+
14
+ interface IncidentsResponse {
15
+ count: number
16
+ total: number
17
+ events: Incident[]
18
+ }
19
+
20
+ function formatTs(iso: string): string {
21
+ try {
22
+ const d = new Date(iso)
23
+ return d.toLocaleDateString() + ' ' + d.toTimeString().slice(0, 8)
24
+ } catch {
25
+ return iso
26
+ }
27
+ }
28
+
29
+ export default function IncidentsPage() {
30
+ const [data, setData] = useState<IncidentsResponse | null>(null)
31
+ const [loading, setLoading] = useState(true)
32
+ const [error, setError] = useState<string | null>(null)
33
+ const [openRow, setOpenRow] = useState<string | null>(null)
34
+
35
+ // ADR-057 #2 — page reads project from URL (deep-linkable) or localStorage,
36
+ // matching AppShell's resolution chain so the incidents view stays in sync
37
+ // with whichever project the operator last selected on the graph view.
38
+ const [project, setProject] = useState<string>('default')
39
+ useEffect(() => {
40
+ if (typeof window === 'undefined') return
41
+ const fromUrl = new URLSearchParams(window.location.search).get('project')
42
+ let stored: string | null = null
43
+ try { stored = window.localStorage.getItem('neat:lastProject') } catch { /* noop */ }
44
+ setProject(fromUrl || stored || 'default')
45
+ }, [])
46
+
47
+ // ADR-057 #3 — re-fetch when project changes.
48
+ useEffect(() => {
49
+ setLoading(true)
50
+ fetch(`/api/incidents?limit=100&project=${encodeURIComponent(project)}`)
51
+ .then((r) => r.json())
52
+ .then((d: IncidentsResponse) => {
53
+ setData(d)
54
+ setLoading(false)
55
+ })
56
+ .catch((e: Error) => {
57
+ setError(e.message)
58
+ setLoading(false)
59
+ })
60
+ }, [project])
61
+
62
+ return (
63
+ <div style={{ background: 'var(--ink-0)', minHeight: '100vh' }}>
64
+ <header className="topbar">
65
+ <div className="brand" title="NEAT">N</div>
66
+ <div className="crumbs">
67
+ <Link href="/" className="incidents-nav-link">graph view</Link>
68
+ <span className="sep">/</span>
69
+ <span className="here">incidents</span>
70
+ </div>
71
+ </header>
72
+
73
+ <div className="incidents-page" style={{ marginTop: 44 }}>
74
+ <h1>Incidents</h1>
75
+ <div className="subtitle">
76
+ {data ? `${data.total} total events — showing ${data.events.length}` : 'loading…'}
77
+ </div>
78
+
79
+ {loading && (
80
+ <div className="incidents-empty">loading…</div>
81
+ )}
82
+
83
+ {error && (
84
+ <div className="incidents-empty" style={{ color: '#e87a7a' }}>
85
+ failed to load: {error}
86
+ </div>
87
+ )}
88
+
89
+ {!loading && !error && data && data.events.length === 0 && (
90
+ <div className="incidents-empty">no incidents recorded</div>
91
+ )}
92
+
93
+ {!loading && !error && data && data.events.length > 0 && (
94
+ <table className="incidents-table">
95
+ <thead>
96
+ <tr>
97
+ <th>Node</th>
98
+ <th>Time</th>
99
+ <th>Type</th>
100
+ <th>Message</th>
101
+ </tr>
102
+ </thead>
103
+ <tbody>
104
+ {data.events.map((evt, i) => {
105
+ const rowKey = `${i}-${evt.nodeId}`
106
+ const isOpen = openRow === rowKey
107
+ return (
108
+ <>
109
+ <tr
110
+ key={rowKey}
111
+ style={{ cursor: evt.stacktrace ? 'pointer' : undefined }}
112
+ onClick={() => evt.stacktrace && setOpenRow(isOpen ? null : rowKey)}
113
+ title={evt.stacktrace ? (isOpen ? 'Collapse stacktrace' : 'Expand stacktrace') : undefined}
114
+ >
115
+ <td className="td-node">
116
+ <Link href={`/?node=${encodeURIComponent(evt.nodeId)}&project=${encodeURIComponent(project)}`} className="incidents-node-link">
117
+ {evt.nodeId}
118
+ </Link>
119
+ </td>
120
+ <td className="td-time">{formatTs(evt.timestamp)}</td>
121
+ <td className="td-type">{evt.type}</td>
122
+ <td className="td-msg">
123
+ {evt.message}
124
+ {evt.stacktrace && (
125
+ <span className="stack-toggle">{isOpen ? ' ▲' : ' ▼'}</span>
126
+ )}
127
+ </td>
128
+ </tr>
129
+ {isOpen && evt.stacktrace && (
130
+ <tr key={`${rowKey}-stack`}>
131
+ <td colSpan={4} className="td-stacktrace">
132
+ <pre className="stacktrace-pre">{evt.stacktrace}</pre>
133
+ </td>
134
+ </tr>
135
+ )}
136
+ </>
137
+ )
138
+ })}
139
+ </tbody>
140
+ </table>
141
+ )}
142
+ </div>
143
+ </div>
144
+ )
145
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,27 @@
1
+ import type { Metadata } from 'next'
2
+ import './globals.css'
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'NEAT',
6
+ description: 'Live semantic graph of your software systems.',
7
+ }
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: Readonly<{
12
+ children: React.ReactNode
13
+ }>) {
14
+ return (
15
+ <html lang="en">
16
+ <head>
17
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
19
+ <link
20
+ href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap"
21
+ rel="stylesheet"
22
+ />
23
+ </head>
24
+ <body>{children}</body>
25
+ </html>
26
+ )
27
+ }
package/app/page.tsx ADDED
@@ -0,0 +1,5 @@
1
+ import { AppShell } from './components/AppShell'
2
+
3
+ export default function Home(): JSX.Element {
4
+ return <AppShell />
5
+ }
@@ -0,0 +1,94 @@
1
+ // Fixture data returned when NEAT_DEMO=1 and core is unreachable.
2
+ // Represents a realistic microservices graph for standalone frontend dev.
3
+
4
+ export const FIXTURE_GRAPH = {
5
+ nodes: [
6
+ { id: 'service:checkout', type: 'ServiceNode', name: 'checkout', language: 'TypeScript', version: '2.4.1' },
7
+ { id: 'service:payments', type: 'ServiceNode', name: 'payments', language: 'Go', version: '1.2.0' },
8
+ { id: 'service:auth', type: 'ServiceNode', name: 'auth', language: 'TypeScript', version: '3.0.0' },
9
+ { id: 'service:api-gateway', type: 'ServiceNode', name: 'api/gateway', language: 'TypeScript', version: '1.0.5' },
10
+ { id: 'service:notifications', type: 'ServiceNode', name: 'notifications', language: 'Python', version: '1.1.0' },
11
+ { id: 'database:payments-db.internal', type: 'DatabaseNode', name: 'payments-db', host: 'payments-db.internal', port: '5432', engine: 'postgresql', engineVersion: '15.2' },
12
+ { id: 'database:auth-db.internal', type: 'DatabaseNode', name: 'auth-db', host: 'auth-db.internal', port: '5432', engine: 'postgresql', engineVersion: '14.8' },
13
+ { id: 'infra:redis:cache.internal', type: 'InfraNode', name: 'cache', kind: 'cache', host: 'cache.internal', port: '6379' },
14
+ ],
15
+ edges: [
16
+ { id: 'CALLS:OBSERVED:service:api-gateway->service:checkout', source: 'service:api-gateway', target: 'service:checkout', type: 'CALLS', provenance: 'OBSERVED', confidence: 1.0, callCount: 42891 },
17
+ { id: 'CALLS:OBSERVED:service:api-gateway->service:auth', source: 'service:api-gateway', target: 'service:auth', type: 'CALLS', provenance: 'OBSERVED', confidence: 1.0, callCount: 18204 },
18
+ { id: 'CALLS:OBSERVED:service:checkout->service:payments', source: 'service:checkout', target: 'service:payments', type: 'CALLS', provenance: 'OBSERVED', confidence: 1.0, callCount: 9341 },
19
+ { id: 'CALLS:EXTRACTED:service:checkout->service:notifications', source: 'service:checkout', target: 'service:notifications', type: 'CALLS', provenance: 'EXTRACTED', confidence: 0.9 },
20
+ { id: 'CONNECTS_TO:OBSERVED:service:payments->database:payments-db.internal', source: 'service:payments', target: 'database:payments-db.internal', type: 'CONNECTS_TO', provenance: 'OBSERVED', confidence: 1.0 },
21
+ { id: 'CONNECTS_TO:EXTRACTED:service:auth->database:auth-db.internal', source: 'service:auth', target: 'database:auth-db.internal', type: 'CONNECTS_TO', provenance: 'EXTRACTED', confidence: 0.95 },
22
+ { id: 'CONNECTS_TO:INFERRED:service:checkout->infra:redis:cache.internal', source: 'service:checkout', target: 'infra:redis:cache.internal', type: 'CONNECTS_TO', provenance: 'INFERRED', confidence: 0.6 },
23
+ { id: 'CONNECTS_TO:INFERRED:service:auth->infra:redis:cache.internal', source: 'service:auth', target: 'infra:redis:cache.internal', type: 'CONNECTS_TO', provenance: 'INFERRED', confidence: 0.6 },
24
+ ],
25
+ }
26
+
27
+ export const FIXTURE_INCIDENTS = {
28
+ count: 3,
29
+ total: 3,
30
+ events: [
31
+ {
32
+ nodeId: 'service:payments',
33
+ timestamp: new Date(Date.now() - 1000 * 60 * 14).toISOString(),
34
+ type: 'ERR_VERSION_MISMATCH',
35
+ message: 'pg driver 7.4.0 incompatible with PostgreSQL 15 — connection failed',
36
+ stacktrace: 'Error: connect ECONNREFUSED\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1187:16)\n at pg.Client.connect (/app/node_modules/pg/lib/client.js:54:9)',
37
+ },
38
+ {
39
+ nodeId: 'service:checkout',
40
+ timestamp: new Date(Date.now() - 1000 * 60 * 38).toISOString(),
41
+ type: 'ERR_TIMEOUT',
42
+ message: 'upstream payments service exceeded 5s timeout on /charge',
43
+ },
44
+ {
45
+ nodeId: 'service:auth',
46
+ timestamp: new Date(Date.now() - 1000 * 60 * 91).toISOString(),
47
+ type: 'ERR_RATE_LIMIT',
48
+ message: 'Redis rate-limit key expired — 429 burst on /token',
49
+ },
50
+ ],
51
+ }
52
+
53
+ export const FIXTURE_HEALTH = { ok: true, project: 'demo' }
54
+
55
+ export const FIXTURE_PROJECTS = [
56
+ { name: 'demo', path: '/workspace/demo', status: 'active' as const },
57
+ ]
58
+
59
+ export const FIXTURE_VIOLATIONS = { violations: [] }
60
+
61
+ export function fixtureSearch(q: string) {
62
+ const lower = q.toLowerCase()
63
+ const results = FIXTURE_GRAPH.nodes
64
+ .filter((n) => n.name.toLowerCase().includes(lower) || n.id.toLowerCase().includes(lower))
65
+ .map((n) => ({ node: { id: n.id, type: n.type, name: n.name }, score: 0.95 }))
66
+ return { results }
67
+ }
68
+
69
+ export function fixtureNodeDetail(id: string) {
70
+ const node = FIXTURE_GRAPH.nodes.find((n) => n.id === id)
71
+ if (!node) return { error: 'not found' }
72
+ return { node }
73
+ }
74
+
75
+ export function fixtureRootCause(id: string) {
76
+ if (id === 'service:payments') {
77
+ return {
78
+ origin: id,
79
+ rootCauseNode: 'database:payments-db.internal',
80
+ reason: 'pg driver 7.4.0 is incompatible with PostgreSQL 15 — protocol mismatch causes connection failure',
81
+ fixRecommendation: 'upgrade pg to ^8.x (supports PostgreSQL 15 protocol)',
82
+ confidence: 0.87,
83
+ traversalPath: [id, 'database:payments-db.internal'],
84
+ }
85
+ }
86
+ return { origin: id, rootCauseNode: null, reason: '', fixRecommendation: null, confidence: 0, traversalPath: [] }
87
+ }
88
+
89
+ export function fixtureBlastRadius(id: string) {
90
+ const downstream = FIXTURE_GRAPH.edges
91
+ .filter((e) => e.source === id)
92
+ .map((e) => ({ nodeId: e.target, distance: 1, confidence: e.confidence, path: [id, e.target] }))
93
+ return { origin: id, affectedNodes: downstream, violationCount: 0 }
94
+ }
package/lib/proxy.ts ADDED
@@ -0,0 +1,16 @@
1
+ export const CORE_URL = process.env.NEAT_CORE_URL ?? 'http://localhost:8080'
2
+ export const DEMO = process.env.NEAT_DEMO === '1'
3
+
4
+ export async function proxyGet(url: string, fallback: () => Response): Promise<Response> {
5
+ try {
6
+ const upstream = await fetch(url, { cache: 'no-store' })
7
+ const body = await upstream.text()
8
+ return new Response(body, {
9
+ status: upstream.status,
10
+ headers: { 'content-type': upstream.headers.get('content-type') ?? 'application/json' },
11
+ })
12
+ } catch {
13
+ if (DEMO) return fallback()
14
+ return Response.json({ error: 'failed to reach neat-core', coreUrl: CORE_URL }, { status: 502 })
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@neat.is/web",
3
+ "version": "0.2.10",
4
+ "description": "NEAT web shell — minimal Next.js app fronting the core API",
5
+ "license": "BUSL-1.1",
6
+ "homepage": "https://neat.is",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/NEAT-Technologies/Neat.git",
10
+ "directory": "packages/web"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "files": [
19
+ "app",
20
+ "lib",
21
+ "public",
22
+ "next.config.js",
23
+ "tsconfig.json",
24
+ "postcss.config.js",
25
+ "tailwind.config.js",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "dev": "next dev",
30
+ "build": "next build",
31
+ "start": "next start",
32
+ "lint": "eslint app",
33
+ "test": "vitest run --passWithNoTests",
34
+ "clean": "rm -rf .next .turbo"
35
+ },
36
+ "dependencies": {
37
+ "@neat.is/types": "^0.2.10",
38
+ "cytoscape": "^3.33.3",
39
+ "next": "14.2.35",
40
+ "react": "^18",
41
+ "react-dom": "^18"
42
+ },
43
+ "devDependencies": {
44
+ "@types/cytoscape": "^3.21.9",
45
+ "@types/node": "^20",
46
+ "@types/react": "^18",
47
+ "@types/react-dom": "^18",
48
+ "postcss": "^8",
49
+ "tailwindcss": "^3.4.1",
50
+ "typescript": "^5",
51
+ "vitest": "^2.1.0"
52
+ }
53
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["dom", "dom.iterable", "esnext"],
4
+ "allowJs": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "esModuleInterop": true,
9
+ "module": "esnext",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "jsx": "preserve",
14
+ "incremental": true,
15
+ "plugins": [
16
+ {
17
+ "name": "next"
18
+ }
19
+ ],
20
+ "paths": {
21
+ "@/*": ["./*"]
22
+ }
23
+ },
24
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25
+ "exclude": ["node_modules"]
26
+ }