@flame0510/project-aether 1.7.0 → 1.9.0
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 +3 -2
- package/app/agents/PageClient.tsx +3 -0
- package/app/agents/create/loading.tsx +51 -0
- package/app/agents/loading.tsx +2 -11
- package/app/api/assistant/route.ts +9 -2
- package/app/api/metrics/route.ts +126 -23
- package/app/api/system-health/route.ts +24 -20
- package/app/components/Sidebar.tsx +10 -0
- package/app/components/Skeleton.tsx +49 -1
- package/app/components/SystemCockpit.tsx +82 -2
- package/app/components/ui/Meter.tsx +34 -0
- package/app/components/ui/TimeSeriesChart.tsx +226 -0
- package/app/components/ui/index.ts +2 -0
- package/app/config/loading.tsx +4 -9
- package/app/containers/ContainersClient.tsx +4 -2
- package/app/containers/loading.tsx +2 -18
- package/app/containers/terminal/[id]/TerminalClient.tsx +230 -276
- package/app/containers/terminal/[id]/loading.tsx +20 -0
- package/app/crons/loading.tsx +4 -9
- package/app/gateway/loading.tsx +3 -7
- package/app/globals.css +51 -0
- package/app/lineage/loading.tsx +4 -9
- package/app/memory/loading.tsx +4 -9
- package/app/plugins/loading.tsx +4 -9
- package/app/skills/loading.tsx +4 -9
- package/app/system/PageClient.tsx +263 -0
- package/app/system/SystemSkeleton.tsx +115 -0
- package/app/system/loading.tsx +13 -0
- package/app/system/page.tsx +5 -0
- package/app/tools/loading.tsx +4 -9
- package/bin/rev4a.js +8 -4
- package/daemon.js +271 -190
- package/docs/ARCHITECTURE.md +49 -46
- package/docs/CONTAINER-TERMINAL.md +169 -261
- package/docs/DESIGN-SYSTEM.md +1 -0
- package/docs/FRONTEND-ARCHITECTURE.md +7 -2
- package/docs/REV4A.md +5 -3
- package/docs/dev/API-REFERENCE.md +61 -16
- package/docs/dev/DATABASE.md +65 -25
- package/docs/rag/DATA-FRESHNESS.md +13 -8
- package/docs/rag/GLOSSARY.md +7 -4
- package/docs/rag/REV4A-OVERVIEW.md +5 -2
- package/docs/rag/WHAT-I-CAN-ANSWER.md +1 -0
- package/lib/db-bootstrap.mjs +0 -11
- package/lib/metrics-db.ts +48 -0
- package/package.json +5 -1
- package/scripts/backup.sh +3 -3
- package/server.js +183 -0
- package/terminal-ws-server.js +329 -96
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent, type PointerEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface TimeSeriesPoint {
|
|
6
|
+
/** Seconds since the epoch — the start of the bucket. */
|
|
7
|
+
ts: number;
|
|
8
|
+
/** The plotted value (an average over the bucket); null leaves a gap. */
|
|
9
|
+
value: number | null;
|
|
10
|
+
/** The bucket's peak, drawn as a light wash under the line and shown in the tooltip. */
|
|
11
|
+
peak?: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface TimeSeriesChartProps {
|
|
15
|
+
/** What is plotted, for screen readers and the table caption. */
|
|
16
|
+
label: string;
|
|
17
|
+
points: TimeSeriesPoint[];
|
|
18
|
+
/** The x domain in seconds, so an empty stretch reads as "no data", not as a zoom. */
|
|
19
|
+
from: number;
|
|
20
|
+
to: number;
|
|
21
|
+
/** Seconds per point; two points further apart than 1.5 buckets are not joined. */
|
|
22
|
+
bucket: number;
|
|
23
|
+
yMax?: number;
|
|
24
|
+
unit?: string;
|
|
25
|
+
height?: number;
|
|
26
|
+
/** While a new range loads the previous render stays, dimmed. */
|
|
27
|
+
stale?: boolean;
|
|
28
|
+
formatTime: (ts: number) => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PAD = { top: 12, right: 44, bottom: 22, left: 34 };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One series over time: a 2px line for the value, a 10% wash up to the peak, a hairline
|
|
35
|
+
* grid at 0 / 50 / 100 %, the latest value labelled at the line's end. Hover or focus
|
|
36
|
+
* shows a crosshair snapped to the nearest point with its value and peak; the same data
|
|
37
|
+
* is available as a table. Plain SVG measured to its container, so text is never scaled.
|
|
38
|
+
*/
|
|
39
|
+
export function TimeSeriesChart({
|
|
40
|
+
label,
|
|
41
|
+
points,
|
|
42
|
+
from,
|
|
43
|
+
to,
|
|
44
|
+
bucket,
|
|
45
|
+
yMax = 100,
|
|
46
|
+
unit = '%',
|
|
47
|
+
height = 160,
|
|
48
|
+
stale = false,
|
|
49
|
+
formatTime,
|
|
50
|
+
}: TimeSeriesChartProps) {
|
|
51
|
+
const boxRef = useRef<HTMLDivElement>(null);
|
|
52
|
+
const clipId = `ts-clip-${useId().replace(/:/g, '')}`;
|
|
53
|
+
const [width, setWidth] = useState(0);
|
|
54
|
+
const [active, setActive] = useState<number | null>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const el = boxRef.current;
|
|
58
|
+
if (!el) return;
|
|
59
|
+
const ro = new ResizeObserver(([entry]) => setWidth(Math.round(entry.contentRect.width)));
|
|
60
|
+
ro.observe(el);
|
|
61
|
+
return () => ro.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const plotW = Math.max(0, width - PAD.left - PAD.right);
|
|
65
|
+
const plotH = height - PAD.top - PAD.bottom;
|
|
66
|
+
const span = Math.max(1, to - from);
|
|
67
|
+
const x = (ts: number) => PAD.left + ((ts - from) / span) * plotW;
|
|
68
|
+
const y = (v: number) => PAD.top + plotH - (Math.max(0, Math.min(yMax, v)) / yMax) * plotH;
|
|
69
|
+
|
|
70
|
+
const valid = useMemo(() => points.filter((p) => p.value !== null), [points]);
|
|
71
|
+
|
|
72
|
+
// Runs of points close enough in time to be joined; a gap means no samples there.
|
|
73
|
+
const runs = useMemo(() => {
|
|
74
|
+
const out: TimeSeriesPoint[][] = [];
|
|
75
|
+
let run: TimeSeriesPoint[] = [];
|
|
76
|
+
for (const p of valid) {
|
|
77
|
+
const prev = run[run.length - 1];
|
|
78
|
+
if (prev && p.ts - prev.ts > bucket * 1.5) {
|
|
79
|
+
out.push(run);
|
|
80
|
+
run = [];
|
|
81
|
+
}
|
|
82
|
+
run.push(p);
|
|
83
|
+
}
|
|
84
|
+
if (run.length) out.push(run);
|
|
85
|
+
return out;
|
|
86
|
+
}, [valid, bucket]);
|
|
87
|
+
|
|
88
|
+
const linePath = (run: TimeSeriesPoint[]) =>
|
|
89
|
+
run.map((p, i) => `${i ? 'L' : 'M'}${x(p.ts).toFixed(1)},${y(p.value as number).toFixed(1)}`).join('');
|
|
90
|
+
const peakArea = (run: TimeSeriesPoint[]) => {
|
|
91
|
+
const top = run.map((p, i) => `${i ? 'L' : 'M'}${x(p.ts).toFixed(1)},${y(p.peak ?? (p.value as number)).toFixed(1)}`).join('');
|
|
92
|
+
const base = y(0).toFixed(1);
|
|
93
|
+
return `${top}L${x(run[run.length - 1].ts).toFixed(1)},${base}L${x(run[0].ts).toFixed(1)},${base}Z`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const last = valid[valid.length - 1];
|
|
97
|
+
const shown = active !== null ? valid[active] : null;
|
|
98
|
+
const fmt = (v: number | null | undefined) => (v === null || v === undefined ? '—' : `${Math.round(v * 10) / 10}${unit}`);
|
|
99
|
+
|
|
100
|
+
function onPointerMove(e: PointerEvent<SVGRectElement>) {
|
|
101
|
+
if (!valid.length || plotW <= 0) return;
|
|
102
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
103
|
+
const ts = from + ((e.clientX - rect.left) / rect.width) * span;
|
|
104
|
+
let best = 0;
|
|
105
|
+
for (let i = 1; i < valid.length; i++) {
|
|
106
|
+
if (Math.abs(valid[i].ts - ts) < Math.abs(valid[best].ts - ts)) best = i;
|
|
107
|
+
}
|
|
108
|
+
setActive(best);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function onKeyDown(e: KeyboardEvent<HTMLDivElement>) {
|
|
112
|
+
if (!valid.length) return;
|
|
113
|
+
if (e.key === 'ArrowLeft') setActive((a) => Math.max(0, (a ?? valid.length) - 1));
|
|
114
|
+
else if (e.key === 'ArrowRight') setActive((a) => Math.min(valid.length - 1, (a ?? -1) + 1));
|
|
115
|
+
else if (e.key === 'Escape') setActive(null);
|
|
116
|
+
else return;
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const ticks = [from, from + span / 2, to];
|
|
121
|
+
const tooltipLeft = shown ? Math.min(Math.max(x(shown.ts), 70), Math.max(70, width - 70)) : 0;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="ui-chart" style={{ opacity: stale ? 0.5 : 1 }}>
|
|
125
|
+
<div
|
|
126
|
+
ref={boxRef}
|
|
127
|
+
className="ui-chart__plot"
|
|
128
|
+
style={{ height }}
|
|
129
|
+
tabIndex={0}
|
|
130
|
+
role="img"
|
|
131
|
+
aria-label={last ? `${label}: latest ${fmt(last.value)}` : `${label}: no data`}
|
|
132
|
+
onKeyDown={onKeyDown}
|
|
133
|
+
onFocus={() => setActive((a) => a ?? (valid.length ? valid.length - 1 : null))}
|
|
134
|
+
onBlur={() => setActive(null)}
|
|
135
|
+
>
|
|
136
|
+
{width > 0 && (
|
|
137
|
+
<svg width={width} height={height} aria-hidden="true">
|
|
138
|
+
{[0, 50, 100].map((v) => (
|
|
139
|
+
<g key={v}>
|
|
140
|
+
<line x1={PAD.left} x2={PAD.left + plotW} y1={y((v / 100) * yMax)} y2={y((v / 100) * yMax)} className="ui-chart__grid" />
|
|
141
|
+
<text x={PAD.left - 6} y={y((v / 100) * yMax) + 3} textAnchor="end" className="ui-chart__tick">
|
|
142
|
+
{Math.round((v / 100) * yMax)}{unit}
|
|
143
|
+
</text>
|
|
144
|
+
</g>
|
|
145
|
+
))}
|
|
146
|
+
{ticks.map((ts, i) => (
|
|
147
|
+
<text
|
|
148
|
+
key={i}
|
|
149
|
+
x={x(ts)}
|
|
150
|
+
y={height - 6}
|
|
151
|
+
textAnchor={i === 0 ? 'start' : i === 2 ? 'end' : 'middle'}
|
|
152
|
+
className="ui-chart__tick"
|
|
153
|
+
>
|
|
154
|
+
{formatTime(ts)}
|
|
155
|
+
</text>
|
|
156
|
+
))}
|
|
157
|
+
<defs>
|
|
158
|
+
{/* Marks stay inside the plot, never over the axis labels. */}
|
|
159
|
+
<clipPath id={clipId}>
|
|
160
|
+
<rect x={PAD.left} y={0} width={plotW} height={PAD.top + plotH} />
|
|
161
|
+
</clipPath>
|
|
162
|
+
</defs>
|
|
163
|
+
<g clipPath={`url(#${clipId})`}>
|
|
164
|
+
{runs.map((run, i) => (
|
|
165
|
+
<g key={i}>
|
|
166
|
+
<path d={peakArea(run)} className="ui-chart__wash" />
|
|
167
|
+
{run.length > 1 && <path d={linePath(run)} className="ui-chart__line" />}
|
|
168
|
+
{run.length === 1 && <circle cx={x(run[0].ts)} cy={y(run[0].value as number)} r={2} className="ui-chart__dot" />}
|
|
169
|
+
</g>
|
|
170
|
+
))}
|
|
171
|
+
</g>
|
|
172
|
+
{last && !shown && (
|
|
173
|
+
<g>
|
|
174
|
+
<circle cx={x(last.ts)} cy={y(last.value as number)} r={4} className="ui-chart__marker" />
|
|
175
|
+
<text x={x(last.ts) + 8} y={y(last.value as number) + 3} className="ui-chart__end-label">{fmt(last.value)}</text>
|
|
176
|
+
</g>
|
|
177
|
+
)}
|
|
178
|
+
{shown && (
|
|
179
|
+
<g>
|
|
180
|
+
<line x1={x(shown.ts)} x2={x(shown.ts)} y1={PAD.top} y2={PAD.top + plotH} className="ui-chart__crosshair" />
|
|
181
|
+
<circle cx={x(shown.ts)} cy={y(shown.value as number)} r={4} className="ui-chart__marker" />
|
|
182
|
+
</g>
|
|
183
|
+
)}
|
|
184
|
+
{!valid.length && (
|
|
185
|
+
<text x={PAD.left + plotW / 2} y={PAD.top + plotH / 2} textAnchor="middle" className="ui-chart__tick">No samples in this range</text>
|
|
186
|
+
)}
|
|
187
|
+
<rect
|
|
188
|
+
x={PAD.left}
|
|
189
|
+
y={PAD.top}
|
|
190
|
+
width={plotW}
|
|
191
|
+
height={plotH}
|
|
192
|
+
fill="transparent"
|
|
193
|
+
onPointerMove={onPointerMove}
|
|
194
|
+
onPointerLeave={() => setActive(null)}
|
|
195
|
+
/>
|
|
196
|
+
</svg>
|
|
197
|
+
)}
|
|
198
|
+
{shown && (
|
|
199
|
+
<div className="ui-chart__tooltip" style={{ left: tooltipLeft }}>
|
|
200
|
+
<strong>{fmt(shown.value)}</strong>
|
|
201
|
+
{shown.peak !== undefined && <span className="ui-muted"> · peak {fmt(shown.peak)}</span>}
|
|
202
|
+
<div className="ui-muted">{formatTime(shown.ts)}</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
<details className="ui-chart__table">
|
|
207
|
+
<summary>Table</summary>
|
|
208
|
+
<table>
|
|
209
|
+
<caption>{label}</caption>
|
|
210
|
+
<thead>
|
|
211
|
+
<tr><th scope="col">Time</th><th scope="col">Value</th>{valid.some((p) => p.peak !== undefined) && <th scope="col">Peak</th>}</tr>
|
|
212
|
+
</thead>
|
|
213
|
+
<tbody>
|
|
214
|
+
{valid.map((p) => (
|
|
215
|
+
<tr key={p.ts}>
|
|
216
|
+
<td>{formatTime(p.ts)}</td>
|
|
217
|
+
<td>{fmt(p.value)}</td>
|
|
218
|
+
{p.peak !== undefined && <td>{fmt(p.peak)}</td>}
|
|
219
|
+
</tr>
|
|
220
|
+
))}
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
</details>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
package/app/config/loading.tsx
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ListPageSkeleton } from '../components/Skeleton';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
<CardRowSkeleton count={5} height={48} />
|
|
7
|
-
</div>
|
|
8
|
-
);
|
|
9
|
-
|
|
10
|
-
export default PageSkeleton;
|
|
3
|
+
export default function ConfigLoading() {
|
|
4
|
+
return <ListPageSkeleton />;
|
|
5
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
4
5
|
import { Button, Pill } from '../components/ui';
|
|
5
6
|
|
|
6
7
|
interface ContainerInfo {
|
|
@@ -85,7 +86,8 @@ export default function ContainersClient() {
|
|
|
85
86
|
{c.state === 'running' ? c.status.replace(/^Up\s+/, '') : c.status}
|
|
86
87
|
</span>
|
|
87
88
|
{c.state === 'running' && (
|
|
88
|
-
|
|
89
|
+
// A client-side navigation (prefetched), not a full page reload.
|
|
90
|
+
<Link
|
|
89
91
|
href={`/containers/terminal/${encodeURIComponent(c.name)}`}
|
|
90
92
|
style={{
|
|
91
93
|
fontSize: 10,
|
|
@@ -100,7 +102,7 @@ export default function ContainersClient() {
|
|
|
100
102
|
}}
|
|
101
103
|
>
|
|
102
104
|
TERMINAL
|
|
103
|
-
</
|
|
105
|
+
</Link>
|
|
104
106
|
)}
|
|
105
107
|
</div>
|
|
106
108
|
|
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ListPageSkeleton } from '../components/Skeleton';
|
|
2
2
|
|
|
3
3
|
export default function ContainersLoading() {
|
|
4
|
-
return
|
|
5
|
-
<div style={{
|
|
6
|
-
padding: '16px 20px',
|
|
7
|
-
maxWidth: 1080,
|
|
8
|
-
margin: '0 auto',
|
|
9
|
-
height: '100%',
|
|
10
|
-
boxSizing: 'border-box',
|
|
11
|
-
overflowY: 'auto',
|
|
12
|
-
WebkitOverflowScrolling: 'touch',
|
|
13
|
-
paddingBottom: 72,
|
|
14
|
-
}}>
|
|
15
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
|
16
|
-
<Skeleton width={140} height={20} accent />
|
|
17
|
-
</div>
|
|
18
|
-
<CardRowSkeleton count={4} height={56} />
|
|
19
|
-
</div>
|
|
20
|
-
);
|
|
4
|
+
return <ListPageSkeleton rows={4} rowHeight={56} titleWidth={140} />;
|
|
21
5
|
}
|