@hasna/logs 0.3.25 → 0.3.27
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.
- package/README.md +33 -10
- package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
- package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/cli/index.js +8511 -177
- package/dist/count-bmj4r2zb.js +10 -0
- package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
- package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
- package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
- package/dist/index-931pbyn5.js +141 -0
- package/dist/index-b5c72f1p.js +7 -0
- package/dist/{index-p1vgwwsz.js → index-bnr19y0h.js} +596 -37
- package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
- package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
- package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
- package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
- package/dist/index-hq6kzaah.js +26 -0
- package/dist/index-j34f36wy.js +5672 -0
- package/dist/index-p4dbdzx4.js +1849 -0
- package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
- package/dist/index-t3x838zw.js +2583 -0
- package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
- package/dist/index.js +2982 -22
- package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
- package/dist/mcp/index.js +1473 -4286
- package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
- package/dist/server/index.js +2944 -417
- package/dist/storage.js +50 -0
- package/package.json +27 -8
- package/biome.json +0 -13
- package/bun.lock +0 -376
- package/dashboard/README.md +0 -73
- package/dashboard/bun.lock +0 -526
- package/dashboard/eslint.config.js +0 -23
- package/dashboard/index.html +0 -13
- package/dashboard/package.json +0 -32
- package/dashboard/src/App.css +0 -184
- package/dashboard/src/App.tsx +0 -49
- package/dashboard/src/api.ts +0 -33
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/react.svg +0 -1
- package/dashboard/src/assets/vite.svg +0 -1
- package/dashboard/src/index.css +0 -111
- package/dashboard/src/main.tsx +0 -10
- package/dashboard/src/pages/Alerts.tsx +0 -69
- package/dashboard/src/pages/Issues.tsx +0 -50
- package/dashboard/src/pages/Perf.tsx +0 -75
- package/dashboard/src/pages/Projects.tsx +0 -67
- package/dashboard/src/pages/Summary.tsx +0 -67
- package/dashboard/src/pages/Tail.tsx +0 -65
- package/dashboard/tsconfig.app.json +0 -28
- package/dashboard/tsconfig.json +0 -7
- package/dashboard/tsconfig.node.json +0 -26
- package/dashboard/vite.config.ts +0 -14
- package/dist/count-x3n7qg3c.js +0 -9
- package/dist/index-5cj74qka.js +0 -10803
- package/dist/index-997bkzr2.js +0 -15
- package/dist/index-kezb178p.js +0 -1241
- package/dist/index-pen6t0yc.js +0 -10794
- package/sdk/package.json +0 -27
- package/sdk/src/index.ts +0 -143
- package/sdk/src/types.ts +0 -56
- package/src/cli/entrypoints.test.ts +0 -63
- package/src/cli/index.ts +0 -471
- package/src/db/index.test.ts +0 -33
- package/src/db/index.ts +0 -189
- package/src/db/migrations/001_alert_rules.ts +0 -21
- package/src/db/migrations/002_issues.ts +0 -21
- package/src/db/migrations/003_retention.ts +0 -15
- package/src/db/migrations/004_page_auth.ts +0 -13
- package/src/db/pg-migrations.ts +0 -167
- package/src/index.ts +0 -1
- package/src/lib/alerts.test.ts +0 -67
- package/src/lib/alerts.ts +0 -117
- package/src/lib/browser-script.test.ts +0 -35
- package/src/lib/browser-script.ts +0 -31
- package/src/lib/compare.test.ts +0 -52
- package/src/lib/compare.ts +0 -85
- package/src/lib/count.test.ts +0 -44
- package/src/lib/count.ts +0 -55
- package/src/lib/diagnose.test.ts +0 -55
- package/src/lib/diagnose.ts +0 -91
- package/src/lib/export.test.ts +0 -66
- package/src/lib/export.ts +0 -65
- package/src/lib/github.ts +0 -38
- package/src/lib/health.test.ts +0 -48
- package/src/lib/health.ts +0 -51
- package/src/lib/ingest.test.ts +0 -57
- package/src/lib/ingest.ts +0 -78
- package/src/lib/issues.test.ts +0 -79
- package/src/lib/issues.ts +0 -70
- package/src/lib/jobs.test.ts +0 -69
- package/src/lib/jobs.ts +0 -63
- package/src/lib/lighthouse.ts +0 -65
- package/src/lib/package-meta.test.ts +0 -43
- package/src/lib/package-meta.ts +0 -80
- package/src/lib/page-auth.test.ts +0 -54
- package/src/lib/page-auth.ts +0 -48
- package/src/lib/parse-time.test.ts +0 -37
- package/src/lib/parse-time.ts +0 -14
- package/src/lib/perf.test.ts +0 -45
- package/src/lib/perf.ts +0 -46
- package/src/lib/projects.test.ts +0 -73
- package/src/lib/projects.ts +0 -69
- package/src/lib/query.test.ts +0 -104
- package/src/lib/query.ts +0 -84
- package/src/lib/retention.test.ts +0 -42
- package/src/lib/retention.ts +0 -62
- package/src/lib/rotate.test.ts +0 -37
- package/src/lib/rotate.ts +0 -27
- package/src/lib/scanner.ts +0 -131
- package/src/lib/scheduler.ts +0 -63
- package/src/lib/session-context.ts +0 -28
- package/src/lib/summarize.test.ts +0 -38
- package/src/lib/summarize.ts +0 -23
- package/src/mcp/http.test.ts +0 -92
- package/src/mcp/http.ts +0 -135
- package/src/mcp/index.test.ts +0 -27
- package/src/mcp/index.ts +0 -444
- package/src/server/index.ts +0 -61
- package/src/server/routes/alerts.ts +0 -32
- package/src/server/routes/issues.ts +0 -43
- package/src/server/routes/jobs.ts +0 -32
- package/src/server/routes/logs.ts +0 -113
- package/src/server/routes/perf.ts +0 -23
- package/src/server/routes/projects.ts +0 -67
- package/src/server/routes/stream.ts +0 -43
- package/src/server/server.test.ts +0 -194
- package/src/types/index.ts +0 -119
- package/tsconfig.json +0 -22
- /package/dashboard/{public → dist}/favicon.svg +0 -0
- /package/dashboard/{public → dist}/icons.svg +0 -0
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
|
3
|
-
import { get, type Project, type PerfSnapshot } from '../api'
|
|
4
|
-
|
|
5
|
-
function ScoreBadge({ score }: { score: number | null }) {
|
|
6
|
-
const color = score === null ? '#64748b' : score >= 90 ? '#4ade80' : score >= 50 ? '#fbbf24' : '#f87171'
|
|
7
|
-
return <span style={{ color, fontWeight: 700, fontSize: 18 }}>{score !== null ? Math.round(score) : '—'}</span>
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function Perf() {
|
|
11
|
-
const [projects, setProjects] = useState<Project[]>([])
|
|
12
|
-
const [selected, setSelected] = useState<string>('')
|
|
13
|
-
const [trend, setTrend] = useState<PerfSnapshot[]>([])
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
get<Project[]>('/projects').then(p => { setProjects(p); if (p[0]) setSelected(p[0].id) }).catch(() => {})
|
|
17
|
-
}, [])
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!selected) return
|
|
21
|
-
get<PerfSnapshot[]>(`/perf/trend?project_id=${selected}&limit=30`).then(setTrend).catch(() => {})
|
|
22
|
-
}, [selected])
|
|
23
|
-
|
|
24
|
-
const chartData = [...trend].reverse().map(s => ({
|
|
25
|
-
time: s.timestamp.slice(5, 16).replace('T', ' '),
|
|
26
|
-
score: s.score !== null ? Math.round(s.score) : null,
|
|
27
|
-
lcp: s.lcp !== null ? Math.round(s.lcp) : null,
|
|
28
|
-
fcp: s.fcp !== null ? Math.round(s.fcp) : null,
|
|
29
|
-
}))
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div>
|
|
33
|
-
<div style={{ display: 'flex', gap: 12, marginBottom: 16, alignItems: 'center' }}>
|
|
34
|
-
<h2 style={{ margin: 0, color: '#38bdf8' }}>Performance</h2>
|
|
35
|
-
<select value={selected} onChange={e => setSelected(e.target.value)} style={{ background: '#1e293b', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4 }}>
|
|
36
|
-
{projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
37
|
-
</select>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
{trend.length > 0 && (
|
|
41
|
-
<div style={{ display: 'flex', gap: 16, marginBottom: 20, flexWrap: 'wrap' }}>
|
|
42
|
-
{[
|
|
43
|
-
{ label: 'Score', value: trend[0]?.score ?? null },
|
|
44
|
-
{ label: 'LCP (ms)', value: trend[0]?.lcp !== null && trend[0]?.lcp !== undefined ? Math.round(trend[0].lcp) : null },
|
|
45
|
-
{ label: 'FCP (ms)', value: trend[0]?.fcp !== null && trend[0]?.fcp !== undefined ? Math.round(trend[0].fcp) : null },
|
|
46
|
-
{ label: 'CLS', value: trend[0]?.cls !== null && trend[0]?.cls !== undefined ? trend[0].cls.toFixed(3) : null },
|
|
47
|
-
].map(stat => (
|
|
48
|
-
<div key={stat.label} style={{ background: '#1e293b', borderRadius: 8, padding: '12px 20px', textAlign: 'center', minWidth: 100 }}>
|
|
49
|
-
<ScoreBadge score={Number(stat.value)} />
|
|
50
|
-
<div style={{ color: '#64748b', fontSize: 12, marginTop: 4 }}>{stat.label}</div>
|
|
51
|
-
</div>
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
)}
|
|
55
|
-
|
|
56
|
-
{chartData.length > 1 ? (
|
|
57
|
-
<div style={{ background: '#1e293b', borderRadius: 8, padding: 20 }}>
|
|
58
|
-
<div style={{ color: '#94a3b8', marginBottom: 12, fontSize: 13 }}>Performance Score Over Time</div>
|
|
59
|
-
<ResponsiveContainer width="100%" height={250}>
|
|
60
|
-
<LineChart data={chartData}>
|
|
61
|
-
<XAxis dataKey="time" stroke="#64748b" tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
|
62
|
-
<YAxis domain={[0, 100]} stroke="#64748b" tick={{ fill: '#94a3b8', fontSize: 12 }} />
|
|
63
|
-
<Tooltip contentStyle={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0' }} />
|
|
64
|
-
<Line type="monotone" dataKey="score" stroke="#38bdf8" strokeWidth={2} dot={false} />
|
|
65
|
-
</LineChart>
|
|
66
|
-
</ResponsiveContainer>
|
|
67
|
-
</div>
|
|
68
|
-
) : (
|
|
69
|
-
<div style={{ color: '#64748b', padding: 40, textAlign: 'center', background: '#1e293b', borderRadius: 8 }}>
|
|
70
|
-
{projects.length === 0 ? 'No projects yet. Register one via CLI or MCP.' : 'No performance data yet. Run a scan job to collect metrics.'}
|
|
71
|
-
</div>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { get, post, type Project } from '../api'
|
|
3
|
-
|
|
4
|
-
export function Projects() {
|
|
5
|
-
const [projects, setProjects] = useState<Project[]>([])
|
|
6
|
-
const [form, setForm] = useState({ name: '', github_repo: '', base_url: '' })
|
|
7
|
-
const [pageForm, setPageForm] = useState({ project_id: '', url: '', name: '' })
|
|
8
|
-
|
|
9
|
-
const load = () => get<Project[]>('/projects').then(setProjects).catch(() => {})
|
|
10
|
-
useEffect(() => { load() }, [])
|
|
11
|
-
|
|
12
|
-
const create = async () => {
|
|
13
|
-
if (!form.name) return
|
|
14
|
-
await post('/projects', form)
|
|
15
|
-
setForm({ name: '', github_repo: '', base_url: '' })
|
|
16
|
-
load()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const addPage = async () => {
|
|
20
|
-
if (!pageForm.project_id || !pageForm.url) return
|
|
21
|
-
await post(`/projects/${pageForm.project_id}/pages`, { url: pageForm.url, name: pageForm.name })
|
|
22
|
-
setPageForm(f => ({ ...f, url: '', name: '' }))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div>
|
|
27
|
-
<h2 style={{ color: '#38bdf8', marginBottom: 16 }}>Projects</h2>
|
|
28
|
-
|
|
29
|
-
<div style={{ background: '#1e293b', borderRadius: 8, padding: 16, marginBottom: 16 }}>
|
|
30
|
-
<div style={{ color: '#94a3b8', fontSize: 13, marginBottom: 10 }}>Register Project</div>
|
|
31
|
-
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
32
|
-
<input placeholder="Name *" value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} style={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4, fontFamily: 'monospace' }} />
|
|
33
|
-
<input placeholder="GitHub repo" value={form.github_repo} onChange={e => setForm(f => ({ ...f, github_repo: e.target.value }))} style={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4, width: 220, fontFamily: 'monospace' }} />
|
|
34
|
-
<input placeholder="Base URL" value={form.base_url} onChange={e => setForm(f => ({ ...f, base_url: e.target.value }))} style={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4, width: 200, fontFamily: 'monospace' }} />
|
|
35
|
-
<button onClick={create} style={{ background: '#1e40af', color: '#e2e8f0', border: 'none', padding: '4px 16px', borderRadius: 4, cursor: 'pointer' }}>+ Create</button>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<div style={{ background: '#1e293b', borderRadius: 8, padding: 16, marginBottom: 20 }}>
|
|
40
|
-
<div style={{ color: '#94a3b8', fontSize: 13, marginBottom: 10 }}>Register Page</div>
|
|
41
|
-
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
42
|
-
<select value={pageForm.project_id} onChange={e => setPageForm(f => ({ ...f, project_id: e.target.value }))} style={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4 }}>
|
|
43
|
-
<option value="">Select project</option>
|
|
44
|
-
{projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
45
|
-
</select>
|
|
46
|
-
<input placeholder="URL *" value={pageForm.url} onChange={e => setPageForm(f => ({ ...f, url: e.target.value }))} style={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4, width: 250, fontFamily: 'monospace' }} />
|
|
47
|
-
<input placeholder="Name" value={pageForm.name} onChange={e => setPageForm(f => ({ ...f, name: e.target.value }))} style={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4, fontFamily: 'monospace' }} />
|
|
48
|
-
<button onClick={addPage} style={{ background: '#1e40af', color: '#e2e8f0', border: 'none', padding: '4px 16px', borderRadius: 4, cursor: 'pointer' }}>+ Add Page</button>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
53
|
-
{projects.length === 0 && <div style={{ color: '#64748b', padding: 20, textAlign: 'center' }}>No projects yet.</div>}
|
|
54
|
-
{projects.map(p => (
|
|
55
|
-
<div key={p.id} style={{ background: '#1e293b', borderRadius: 8, padding: '12px 16px' }}>
|
|
56
|
-
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
|
57
|
-
<span style={{ color: '#e2e8f0', fontWeight: 600 }}>{p.name}</span>
|
|
58
|
-
<span style={{ color: '#475569', fontSize: 11 }}>{p.id}</span>
|
|
59
|
-
{p.base_url && <a href={p.base_url} target="_blank" rel="noreferrer" style={{ color: '#38bdf8', fontSize: 12 }}>{p.base_url}</a>}
|
|
60
|
-
{p.github_repo && <span style={{ color: '#7dd3fc', fontSize: 12 }}>⎋ {p.github_repo}</span>}
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
))}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'
|
|
3
|
-
import { get, type LogSummary, type Health } from '../api'
|
|
4
|
-
|
|
5
|
-
export function Summary() {
|
|
6
|
-
const [summary, setSummary] = useState<LogSummary[]>([])
|
|
7
|
-
const [health, setHealth] = useState<Health | null>(null)
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
get<LogSummary[]>('/logs/summary?since=' + new Date(Date.now() - 24 * 3600 * 1000).toISOString()).then(setSummary).catch(() => {})
|
|
11
|
-
fetch('/health').then(r => r.json()).then(setHealth).catch(() => {})
|
|
12
|
-
}, [])
|
|
13
|
-
|
|
14
|
-
// Group by service for chart
|
|
15
|
-
const byService = summary.reduce<Record<string, { service: string; error: number; warn: number; fatal: number }>>((acc, s) => {
|
|
16
|
-
const svc = s.service ?? 'unknown'
|
|
17
|
-
if (!acc[svc]) acc[svc] = { service: svc, error: 0, warn: 0, fatal: 0 }
|
|
18
|
-
if (s.level === 'error') acc[svc]!.error += s.count
|
|
19
|
-
if (s.level === 'warn') acc[svc]!.warn += s.count
|
|
20
|
-
if (s.level === 'fatal') acc[svc]!.fatal += s.count
|
|
21
|
-
return acc
|
|
22
|
-
}, {})
|
|
23
|
-
const chartData = Object.values(byService).sort((a, b) => (b.error + b.fatal) - (a.error + a.fatal))
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div>
|
|
27
|
-
<h2 style={{ color: '#38bdf8', marginBottom: 16 }}>Summary (last 24h)</h2>
|
|
28
|
-
|
|
29
|
-
{health && (
|
|
30
|
-
<div style={{ display: 'flex', gap: 16, marginBottom: 24, flexWrap: 'wrap' }}>
|
|
31
|
-
{[
|
|
32
|
-
{ label: 'Total Logs', value: health.total_logs.toLocaleString(), color: '#38bdf8' },
|
|
33
|
-
{ label: 'Projects', value: health.projects, color: '#7dd3fc' },
|
|
34
|
-
{ label: 'Open Issues', value: health.open_issues, color: health.open_issues > 0 ? '#f87171' : '#4ade80' },
|
|
35
|
-
{ label: 'Errors', value: (health.logs_by_level['error'] ?? 0) + (health.logs_by_level['fatal'] ?? 0), color: '#f87171' },
|
|
36
|
-
{ label: 'Warnings', value: health.logs_by_level['warn'] ?? 0, color: '#fbbf24' },
|
|
37
|
-
{ label: 'Uptime', value: `${Math.floor(health.uptime_seconds / 60)}m`, color: '#4ade80' },
|
|
38
|
-
].map(stat => (
|
|
39
|
-
<div key={stat.label} style={{ background: '#1e293b', borderRadius: 8, padding: '12px 20px', minWidth: 120, textAlign: 'center' }}>
|
|
40
|
-
<div style={{ color: stat.color, fontSize: 24, fontWeight: 700 }}>{stat.value}</div>
|
|
41
|
-
<div style={{ color: '#64748b', fontSize: 12, marginTop: 4 }}>{stat.label}</div>
|
|
42
|
-
</div>
|
|
43
|
-
))}
|
|
44
|
-
</div>
|
|
45
|
-
)}
|
|
46
|
-
|
|
47
|
-
{chartData.length > 0 ? (
|
|
48
|
-
<div style={{ background: '#1e293b', borderRadius: 8, padding: 20 }}>
|
|
49
|
-
<div style={{ color: '#94a3b8', marginBottom: 12, fontSize: 13 }}>Errors & Warnings by Service</div>
|
|
50
|
-
<ResponsiveContainer width="100%" height={300}>
|
|
51
|
-
<BarChart data={chartData}>
|
|
52
|
-
<XAxis dataKey="service" stroke="#64748b" tick={{ fill: '#94a3b8', fontSize: 12 }} />
|
|
53
|
-
<YAxis stroke="#64748b" tick={{ fill: '#94a3b8', fontSize: 12 }} />
|
|
54
|
-
<Tooltip contentStyle={{ background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0' }} />
|
|
55
|
-
<Legend wrapperStyle={{ color: '#94a3b8' }} />
|
|
56
|
-
<Bar dataKey="fatal" fill="#c084fc" stackId="a" />
|
|
57
|
-
<Bar dataKey="error" fill="#f87171" stackId="a" />
|
|
58
|
-
<Bar dataKey="warn" fill="#fbbf24" stackId="a" />
|
|
59
|
-
</BarChart>
|
|
60
|
-
</ResponsiveContainer>
|
|
61
|
-
</div>
|
|
62
|
-
) : (
|
|
63
|
-
<div style={{ color: '#64748b', padding: 40, textAlign: 'center', background: '#1e293b', borderRadius: 8 }}>No errors or warnings in the last 24h 🎉</div>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import type { LogRow } from '../api'
|
|
3
|
-
|
|
4
|
-
const LEVEL_COLOR: Record<string, string> = {
|
|
5
|
-
debug: '#64748b', info: '#22d3ee', warn: '#fbbf24', error: '#f87171', fatal: '#c084fc'
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function Tail() {
|
|
9
|
-
const [logs, setLogs] = useState<LogRow[]>([])
|
|
10
|
-
const [paused, setPaused] = useState(false)
|
|
11
|
-
const [filter, setFilter] = useState('')
|
|
12
|
-
const bottomRef = useRef<HTMLDivElement>(null)
|
|
13
|
-
const esRef = useRef<EventSource | null>(null)
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
const es = new EventSource('/api/logs/stream')
|
|
17
|
-
esRef.current = es
|
|
18
|
-
es.onmessage = (e) => {
|
|
19
|
-
if (paused) return
|
|
20
|
-
try {
|
|
21
|
-
const log = JSON.parse(e.data) as LogRow
|
|
22
|
-
setLogs(prev => [...prev.slice(-499), log])
|
|
23
|
-
} catch {}
|
|
24
|
-
}
|
|
25
|
-
return () => es.close()
|
|
26
|
-
}, [paused])
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (!paused) bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
30
|
-
}, [logs, paused])
|
|
31
|
-
|
|
32
|
-
const filtered = filter
|
|
33
|
-
? logs.filter(l => l.message.toLowerCase().includes(filter.toLowerCase()) || (l.service ?? '').includes(filter))
|
|
34
|
-
: logs
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div>
|
|
38
|
-
<div style={{ display: 'flex', gap: 12, marginBottom: 12, alignItems: 'center' }}>
|
|
39
|
-
<h2 style={{ margin: 0, color: '#38bdf8' }}>Live Tail</h2>
|
|
40
|
-
<input
|
|
41
|
-
placeholder="Filter..."
|
|
42
|
-
value={filter}
|
|
43
|
-
onChange={e => setFilter(e.target.value)}
|
|
44
|
-
style={{ background: '#1e293b', border: '1px solid #334155', color: '#e2e8f0', padding: '4px 8px', borderRadius: 4, fontFamily: 'monospace' }}
|
|
45
|
-
/>
|
|
46
|
-
<button onClick={() => setPaused(p => !p)} style={{ background: paused ? '#22d3ee' : '#334155', color: paused ? '#0f172a' : '#e2e8f0', border: 'none', padding: '4px 12px', borderRadius: 4, cursor: 'pointer' }}>
|
|
47
|
-
{paused ? '▶ Resume' : '⏸ Pause'}
|
|
48
|
-
</button>
|
|
49
|
-
<button onClick={() => setLogs([])} style={{ background: '#334155', color: '#e2e8f0', border: 'none', padding: '4px 12px', borderRadius: 4, cursor: 'pointer' }}>Clear</button>
|
|
50
|
-
<span style={{ color: '#64748b', fontSize: 12 }}>{filtered.length} logs</span>
|
|
51
|
-
</div>
|
|
52
|
-
<div style={{ background: '#020617', borderRadius: 8, padding: 12, height: 'calc(100vh - 160px)', overflowY: 'auto', fontSize: 13 }}>
|
|
53
|
-
{filtered.map(log => (
|
|
54
|
-
<div key={log.id} style={{ display: 'flex', gap: 12, marginBottom: 2, lineHeight: 1.5 }}>
|
|
55
|
-
<span style={{ color: '#475569', minWidth: 200 }}>{log.timestamp.slice(0, 19).replace('T', ' ')}</span>
|
|
56
|
-
<span style={{ color: LEVEL_COLOR[log.level] ?? '#e2e8f0', minWidth: 50, fontWeight: 700 }}>{log.level.toUpperCase()}</span>
|
|
57
|
-
<span style={{ color: '#7dd3fc', minWidth: 100 }}>{log.service ?? '-'}</span>
|
|
58
|
-
<span style={{ color: '#e2e8f0', wordBreak: 'break-all' }}>{log.message}</span>
|
|
59
|
-
</div>
|
|
60
|
-
))}
|
|
61
|
-
<div ref={bottomRef} />
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
-
"target": "ES2023",
|
|
5
|
-
"useDefineForClassFields": true,
|
|
6
|
-
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
7
|
-
"module": "ESNext",
|
|
8
|
-
"types": ["vite/client"],
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
|
|
11
|
-
/* Bundler mode */
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"moduleDetection": "force",
|
|
16
|
-
"noEmit": true,
|
|
17
|
-
"jsx": "react-jsx",
|
|
18
|
-
|
|
19
|
-
/* Linting */
|
|
20
|
-
"strict": true,
|
|
21
|
-
"noUnusedLocals": true,
|
|
22
|
-
"noUnusedParameters": true,
|
|
23
|
-
"erasableSyntaxOnly": true,
|
|
24
|
-
"noFallthroughCasesInSwitch": true,
|
|
25
|
-
"noUncheckedSideEffectImports": true
|
|
26
|
-
},
|
|
27
|
-
"include": ["src"]
|
|
28
|
-
}
|
package/dashboard/tsconfig.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
-
"target": "ES2023",
|
|
5
|
-
"lib": ["ES2023"],
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
|
|
10
|
-
/* Bundler mode */
|
|
11
|
-
"moduleResolution": "bundler",
|
|
12
|
-
"allowImportingTsExtensions": true,
|
|
13
|
-
"verbatimModuleSyntax": true,
|
|
14
|
-
"moduleDetection": "force",
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
/* Linting */
|
|
18
|
-
"strict": true,
|
|
19
|
-
"noUnusedLocals": true,
|
|
20
|
-
"noUnusedParameters": true,
|
|
21
|
-
"erasableSyntaxOnly": true,
|
|
22
|
-
"noFallthroughCasesInSwitch": true,
|
|
23
|
-
"noUncheckedSideEffectImports": true
|
|
24
|
-
},
|
|
25
|
-
"include": ["vite.config.ts"]
|
|
26
|
-
}
|
package/dashboard/vite.config.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite'
|
|
2
|
-
import react from '@vitejs/plugin-react'
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
plugins: [react()],
|
|
6
|
-
base: '/dashboard/',
|
|
7
|
-
build: { outDir: 'dist' },
|
|
8
|
-
server: {
|
|
9
|
-
proxy: {
|
|
10
|
-
'/api': 'http://localhost:3460',
|
|
11
|
-
'/health': 'http://localhost:3460',
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
})
|