ui/dashboard #2
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,6 +17,8 @@ __pycache__/
|
||||
ui/src/ui/__screenshots__/
|
||||
ui/playwright-report/
|
||||
ui/test-results/
|
||||
packages/dashboard-next/.next/
|
||||
packages/dashboard-next/out/
|
||||
|
||||
# Mise configuration files
|
||||
mise.toml
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
"check": "pnpm format:check && pnpm tsgo && pnpm lint",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-links",
|
||||
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
|
||||
"dashboard-next:build": "pnpm --dir packages/dashboard-next build",
|
||||
"dashboard-next:dev": "pnpm --dir packages/dashboard-next dev",
|
||||
"dashboard-next:start": "pnpm --dir packages/dashboard-next start",
|
||||
"deadcode:ci": "pnpm deadcode:report:ci:knip && pnpm deadcode:report:ci:ts-prune && pnpm deadcode:report:ci:ts-unused",
|
||||
"deadcode:knip": "pnpm dlx knip --no-progress",
|
||||
"deadcode:report": "pnpm deadcode:knip; pnpm deadcode:ts-prune; pnpm deadcode:ts-unused",
|
||||
|
||||
9
packages/dashboard-gateway-client/package.json
Normal file
9
packages/dashboard-gateway-client/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@openclaw/dashboard-gateway-client",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
249
packages/dashboard-gateway-client/src/index.ts
Normal file
249
packages/dashboard-gateway-client/src/index.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
export type GatewayClientEventFrame = {
|
||||
type: "event";
|
||||
event: string;
|
||||
payload?: unknown;
|
||||
seq?: number;
|
||||
};
|
||||
|
||||
export type GatewayClientResponseFrame = {
|
||||
type: "res";
|
||||
id: string;
|
||||
ok: boolean;
|
||||
payload?: unknown;
|
||||
error?: { code?: string; message?: string; details?: unknown };
|
||||
};
|
||||
|
||||
export type GatewayClientHelloOk = {
|
||||
type: "hello-ok";
|
||||
protocol: number;
|
||||
features?: { methods?: string[]; events?: string[] };
|
||||
snapshot?: unknown;
|
||||
};
|
||||
|
||||
const PROTOCOL_VERSION = 3;
|
||||
|
||||
type GatewayClientConnectParams = {
|
||||
minProtocol: number;
|
||||
maxProtocol: number;
|
||||
auth?: { token?: string; password?: string };
|
||||
client: {
|
||||
id: string;
|
||||
version: string;
|
||||
mode: string;
|
||||
platform: string;
|
||||
displayName?: string;
|
||||
instanceId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type PendingRequest = {
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: unknown) => void;
|
||||
};
|
||||
|
||||
export type GatewayClientOptions = {
|
||||
gatewayUrl: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
onOpen?: () => void;
|
||||
onClose?: (event: CloseEvent) => void;
|
||||
onEvent?: (event: GatewayClientEventFrame) => void;
|
||||
onHello?: (hello: GatewayClientHelloOk) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onGap?: (args: { expected: number; received: number }) => void;
|
||||
reconnect?: boolean;
|
||||
};
|
||||
|
||||
const CONNECT_TIMEOUT_MS = 12_000;
|
||||
|
||||
function createRequestId() {
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
export class DashboardGatewayClient {
|
||||
private readonly options: GatewayClientOptions;
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectTimer: number | null = null;
|
||||
private stopped = false;
|
||||
private backoffMs = 800;
|
||||
private pending = new Map<string, PendingRequest>();
|
||||
private lastSeq: number | null = null;
|
||||
|
||||
constructor(options: GatewayClientOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stopped = false;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
if (this.reconnectTimer !== null) {
|
||||
window.clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.ws) {
|
||||
this.ws.close(1000, "client stop");
|
||||
this.ws = null;
|
||||
}
|
||||
for (const pending of this.pending.values()) {
|
||||
pending.reject(new Error("gateway client stopped"));
|
||||
}
|
||||
this.pending.clear();
|
||||
}
|
||||
|
||||
request<T = unknown>(method: string, params?: unknown): Promise<T> {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return Promise.reject(new Error("gateway not connected"));
|
||||
}
|
||||
const id = createRequestId();
|
||||
const frame = { type: "req", id, method, params };
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
this.pending.set(id, { resolve: (value) => resolve(value as T), reject });
|
||||
});
|
||||
this.ws.send(JSON.stringify(frame));
|
||||
return promise;
|
||||
}
|
||||
|
||||
private connect() {
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(this.options.gatewayUrl);
|
||||
this.ws = ws;
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
this.options.onOpen?.();
|
||||
void this.sendConnect();
|
||||
});
|
||||
|
||||
ws.addEventListener("error", () => {
|
||||
this.options.onError?.(new Error("gateway websocket error"));
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
if (typeof event.data !== "string") {
|
||||
return;
|
||||
}
|
||||
this.handleMessage(event.data);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
if (this.ws === ws) {
|
||||
this.ws = null;
|
||||
}
|
||||
this.options.onClose?.(event);
|
||||
for (const pending of this.pending.values()) {
|
||||
pending.reject(new Error(`gateway disconnected (${event.code})`));
|
||||
}
|
||||
this.pending.clear();
|
||||
if (this.stopped || this.options.reconnect === false) {
|
||||
return;
|
||||
}
|
||||
const wait = this.backoffMs;
|
||||
this.backoffMs = Math.min(10_000, Math.round(this.backoffMs * 1.75));
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.connect();
|
||||
}, wait);
|
||||
});
|
||||
}
|
||||
|
||||
private async sendConnect() {
|
||||
const ws = this.ws;
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectId = createRequestId();
|
||||
const connectParams: GatewayClientConnectParams = {
|
||||
minProtocol: PROTOCOL_VERSION,
|
||||
maxProtocol: PROTOCOL_VERSION,
|
||||
auth:
|
||||
this.options.token || this.options.password
|
||||
? {
|
||||
token: this.options.token,
|
||||
password: this.options.password,
|
||||
}
|
||||
: undefined,
|
||||
client: {
|
||||
id: "openclaw-control-ui",
|
||||
version: "next-preview-0",
|
||||
mode: "ui",
|
||||
platform: typeof navigator === "undefined" ? "browser" : navigator.userAgent,
|
||||
displayName: "Next Preview Dashboard",
|
||||
},
|
||||
};
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({ type: "req", id: connectId, method: "connect", params: connectParams }),
|
||||
);
|
||||
|
||||
const timeout = window.setTimeout(() => {
|
||||
if (this.ws === ws) {
|
||||
ws.close(1008, "connect timeout");
|
||||
}
|
||||
}, CONNECT_TIMEOUT_MS);
|
||||
|
||||
this.pending.set(connectId, {
|
||||
resolve: (value) => {
|
||||
window.clearTimeout(timeout);
|
||||
this.backoffMs = 800;
|
||||
const hello = value as GatewayClientHelloOk;
|
||||
this.options.onHello?.(hello);
|
||||
},
|
||||
reject: (error) => {
|
||||
window.clearTimeout(timeout);
|
||||
this.options.onError?.(
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(typeof error === "string" ? error : "connect failed"),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleMessage(raw: string) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = parsed as { type?: unknown };
|
||||
|
||||
if (frame.type === "event") {
|
||||
const event = parsed as GatewayClientEventFrame;
|
||||
if (typeof event.seq === "number") {
|
||||
if (this.lastSeq !== null && event.seq > this.lastSeq + 1) {
|
||||
this.options.onGap?.({ expected: this.lastSeq + 1, received: event.seq });
|
||||
}
|
||||
this.lastSeq = event.seq;
|
||||
}
|
||||
this.options.onEvent?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.type === "res") {
|
||||
const response = parsed as GatewayClientResponseFrame;
|
||||
const pending = this.pending.get(response.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
this.pending.delete(response.id);
|
||||
if (response.ok) {
|
||||
pending.resolve(response.payload);
|
||||
} else {
|
||||
pending.reject(new Error(response.error?.message ?? "request failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/dashboard-gateway-client/tsconfig.json
Normal file
8
packages/dashboard-gateway-client/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
5
packages/dashboard-next/.env.example
Normal file
5
packages/dashboard-next/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Default gateway WebSocket target for local development
|
||||
NEXT_PUBLIC_GATEWAY_URL=ws://127.0.0.1:18789
|
||||
|
||||
# Optional: base path if hosted behind a prefix (example: /openclaw)
|
||||
NEXT_PUBLIC_BASE_PATH=
|
||||
29
packages/dashboard-next/README.md
Normal file
29
packages/dashboard-next/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# OpenClaw Dashboard (Next preview)
|
||||
|
||||
This is a **phase 1, low-lift preview** of a Next.js dashboard that coexists with the current Lit Control UI.
|
||||
|
||||
## Security posture (phase 1)
|
||||
|
||||
- Gateway remains the source of truth for auth, pairing, scopes, and method authorization.
|
||||
- This app is a browser client only; it does not introduce privileged Next API routes.
|
||||
- URL bootstrap behavior:
|
||||
- `token` may be consumed and persisted locally.
|
||||
- `password` is scrubbed from URL and never hydrated from URL.
|
||||
- `gatewayUrl` may be consumed for remote/Tailscale testing and persisted locally.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
pnpm dashboard-next:dev
|
||||
```
|
||||
|
||||
## Phase 1 Checklist
|
||||
|
||||
- [x] App scaffold with App Router + TS
|
||||
- [x] Shared WS gateway client package
|
||||
- [x] URL bootstrap + sanitization path
|
||||
- [x] Overview route (read-only status/snapshot)
|
||||
- [x] Chat route (minimal send path)
|
||||
- [ ] Feature flag and integrated gateway-serving route
|
||||
- [ ] Parity checks vs existing Control UI tabs
|
||||
- [ ] Security/performance gate review
|
||||
5
packages/dashboard-next/app/chat/page.tsx
Normal file
5
packages/dashboard-next/app/chat/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ChatPanel } from "../../components/chat-panel";
|
||||
|
||||
export default function ChatPage() {
|
||||
return <ChatPanel />;
|
||||
}
|
||||
112
packages/dashboard-next/app/globals.css
Normal file
112
packages/dashboard-next/app/globals.css
Normal file
@@ -0,0 +1,112 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: #0b0d10;
|
||||
color: #f5f7fa;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
border-bottom: 1px solid #232830;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.topbar nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tab-link {
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e3642;
|
||||
background: #171c23;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #2a303a;
|
||||
border-radius: 12px;
|
||||
background: #141920;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.panel + .panel {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #9fa8b5;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff8f8f;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
border: 1px solid #344051;
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
background: #10151b;
|
||||
}
|
||||
|
||||
.chat-log {
|
||||
max-height: 48vh;
|
||||
overflow: auto;
|
||||
border: 1px solid #252c36;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
background: #0f1419;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-row input {
|
||||
flex: 1;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #344051;
|
||||
background: #0e1318;
|
||||
color: #f5f7fa;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.input-row button {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #3d4f66;
|
||||
background: #1b2736;
|
||||
color: #f5f7fa;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
29
packages/dashboard-next/app/layout.tsx
Normal file
29
packages/dashboard-next/app/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import "./globals.css";
|
||||
import { GatewayProvider } from "../components/gateway-provider";
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<GatewayProvider>
|
||||
<div className="app-shell">
|
||||
<header className="topbar">
|
||||
<strong>OpenClaw Dashboard (Next preview)</strong>
|
||||
<nav>
|
||||
<Link className="tab-link" href="/overview">
|
||||
Overview
|
||||
</Link>
|
||||
<Link className="tab-link" href="/chat">
|
||||
Chat
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</GatewayProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
5
packages/dashboard-next/app/overview/page.tsx
Normal file
5
packages/dashboard-next/app/overview/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { OverviewPanel } from "../../components/overview-panel";
|
||||
|
||||
export default function OverviewPage() {
|
||||
return <OverviewPanel />;
|
||||
}
|
||||
5
packages/dashboard-next/app/page.tsx
Normal file
5
packages/dashboard-next/app/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function HomePage() {
|
||||
redirect("/overview");
|
||||
}
|
||||
98
packages/dashboard-next/components/chat-panel.tsx
Normal file
98
packages/dashboard-next/components/chat-panel.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useGateway } from "./gateway-provider";
|
||||
|
||||
type ChatEventPayload = {
|
||||
runId?: string;
|
||||
state?: string;
|
||||
delta?: string;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export function ChatPanel() {
|
||||
const gateway = useGateway();
|
||||
const [sessionKey, setSessionKey] = useState("main");
|
||||
const [message, setMessage] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [logLines, setLogLines] = useState<string[]>([]);
|
||||
|
||||
const latestChatEvent =
|
||||
gateway.lastEvent?.event === "chat"
|
||||
? (gateway.lastEvent.payload as ChatEventPayload | undefined)
|
||||
: null;
|
||||
|
||||
async function onSend() {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed || submitting) {
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setLogLines((prev) => [`You: ${trimmed}`, ...prev].slice(0, 120));
|
||||
setMessage("");
|
||||
try {
|
||||
await gateway.request("chat.send", {
|
||||
sessionKey,
|
||||
message: trimmed,
|
||||
});
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
setLogLines((prev) => [`Error: ${text}`, ...prev].slice(0, 120));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<h2>Chat (preview)</h2>
|
||||
<p className="muted">
|
||||
Minimal phase-1 chat path. Uses existing gateway method/event flow without introducing new
|
||||
privileged API surfaces.
|
||||
</p>
|
||||
|
||||
<div className="input-row">
|
||||
<input
|
||||
value={sessionKey}
|
||||
onChange={(event) => setSessionKey(event.target.value)}
|
||||
placeholder="session key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-row">
|
||||
<input
|
||||
value={message}
|
||||
onChange={(event) => setMessage(event.target.value)}
|
||||
placeholder="Type a message"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
void onSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onSend()}
|
||||
disabled={submitting || !gateway.connected}
|
||||
>
|
||||
{submitting ? "Sending..." : "Send"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="panel" style={{ marginTop: 12 }}>
|
||||
<h3>Last chat event</h3>
|
||||
<pre>{JSON.stringify(latestChatEvent, null, 2) || "(none)"}</pre>
|
||||
</div>
|
||||
|
||||
<div className="panel" style={{ marginTop: 12 }}>
|
||||
<h3>Local transcript</h3>
|
||||
<div className="chat-log">
|
||||
{logLines.length === 0 ? <p className="muted">No messages yet.</p> : null}
|
||||
{logLines.map((line, index) => (
|
||||
<div key={`${line}-${index}`}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
124
packages/dashboard-next/components/gateway-provider.tsx
Normal file
124
packages/dashboard-next/components/gateway-provider.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DashboardGatewayClient,
|
||||
type GatewayClientEventFrame,
|
||||
type GatewayClientHelloOk,
|
||||
} from "@openclaw/dashboard-gateway-client";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
loadStoredGatewayUrl,
|
||||
loadStoredToken,
|
||||
storeGatewayUrl,
|
||||
storeToken,
|
||||
} from "../lib/local-settings";
|
||||
import { consumeBootstrapUrlState } from "../lib/url-state";
|
||||
|
||||
type GatewayState = {
|
||||
connected: boolean;
|
||||
connecting: boolean;
|
||||
lastError: string | null;
|
||||
hello: GatewayClientHelloOk | null;
|
||||
lastEvent: GatewayClientEventFrame | null;
|
||||
request: <T = unknown>(method: string, params?: unknown) => Promise<T>;
|
||||
};
|
||||
|
||||
const GatewayContext = createContext<GatewayState | null>(null);
|
||||
|
||||
function resolveDefaultGatewayUrl() {
|
||||
return process.env.NEXT_PUBLIC_GATEWAY_URL ?? "ws://127.0.0.1:18789";
|
||||
}
|
||||
|
||||
export function GatewayProvider({ children }: { children: ReactNode }) {
|
||||
const clientRef = useRef<DashboardGatewayClient | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [connecting, setConnecting] = useState(true);
|
||||
const [lastError, setLastError] = useState<string | null>(null);
|
||||
const [hello, setHello] = useState<GatewayClientHelloOk | null>(null);
|
||||
const [lastEvent, setLastEvent] = useState<GatewayClientEventFrame | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const bootstrap = consumeBootstrapUrlState();
|
||||
const token = bootstrap.token || loadStoredToken();
|
||||
const gatewayUrl = bootstrap.gatewayUrl || loadStoredGatewayUrl() || resolveDefaultGatewayUrl();
|
||||
|
||||
if (bootstrap.token) {
|
||||
storeToken(bootstrap.token);
|
||||
}
|
||||
if (bootstrap.gatewayUrl) {
|
||||
storeGatewayUrl(bootstrap.gatewayUrl);
|
||||
}
|
||||
|
||||
const client = new DashboardGatewayClient({
|
||||
gatewayUrl,
|
||||
token: token || undefined,
|
||||
reconnect: true,
|
||||
onOpen: () => {
|
||||
setConnecting(true);
|
||||
},
|
||||
onHello: (nextHello) => {
|
||||
setHello(nextHello);
|
||||
setConnected(true);
|
||||
setConnecting(false);
|
||||
setLastError(null);
|
||||
},
|
||||
onEvent: (event) => {
|
||||
setLastEvent(event);
|
||||
},
|
||||
onClose: () => {
|
||||
setConnected(false);
|
||||
setConnecting(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
setLastError(error.message || "gateway error");
|
||||
},
|
||||
onGap: ({ expected, received }) => {
|
||||
setLastError(`event gap detected (expected ${expected}, got ${received})`);
|
||||
},
|
||||
});
|
||||
|
||||
clientRef.current = client;
|
||||
client.start();
|
||||
|
||||
return () => {
|
||||
client.stop();
|
||||
clientRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = useMemo<GatewayState>(
|
||||
() => ({
|
||||
connected,
|
||||
connecting,
|
||||
lastError,
|
||||
hello,
|
||||
lastEvent,
|
||||
request: async (method, params) => {
|
||||
const client = clientRef.current;
|
||||
if (!client) {
|
||||
throw new Error("gateway client unavailable");
|
||||
}
|
||||
return client.request(method, params);
|
||||
},
|
||||
}),
|
||||
[connected, connecting, hello, lastError, lastEvent],
|
||||
);
|
||||
|
||||
return <GatewayContext.Provider value={value}>{children}</GatewayContext.Provider>;
|
||||
}
|
||||
|
||||
export function useGateway() {
|
||||
const context = useContext(GatewayContext);
|
||||
if (!context) {
|
||||
throw new Error("useGateway must be used inside <GatewayProvider>");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
34
packages/dashboard-next/components/overview-panel.tsx
Normal file
34
packages/dashboard-next/components/overview-panel.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useGateway } from "./gateway-provider";
|
||||
|
||||
export function OverviewPanel() {
|
||||
const gateway = useGateway();
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="panel">
|
||||
<h2>Connection</h2>
|
||||
<div className="status-row">
|
||||
<span className="status-pill">{gateway.connected ? "Connected" : "Disconnected"}</span>
|
||||
<span className="status-pill">{gateway.connecting ? "Reconnecting" : "Stable"}</span>
|
||||
</div>
|
||||
{gateway.lastError ? <p className="error">{gateway.lastError}</p> : null}
|
||||
<p className="muted">
|
||||
Phase 1 preview: Gateway is still the security/control plane. This UI does not bypass
|
||||
auth, pairing, or scope checks.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h2>Hello snapshot</h2>
|
||||
<pre>{JSON.stringify(gateway.hello, null, 2) || "(waiting for hello-ok)"}</pre>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h2>Latest event</h2>
|
||||
<pre>{JSON.stringify(gateway.lastEvent, null, 2) || "(no events yet)"}</pre>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
packages/dashboard-next/lib/local-settings.ts
Normal file
40
packages/dashboard-next/lib/local-settings.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
const TOKEN_KEY = "openclaw.dashboard-next.token";
|
||||
const GATEWAY_URL_KEY = "openclaw.dashboard-next.gateway-url";
|
||||
|
||||
export function loadStoredToken() {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
return window.localStorage.getItem(TOKEN_KEY) ?? "";
|
||||
}
|
||||
|
||||
export function storeToken(token: string) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
if (!token.trim()) {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(TOKEN_KEY, token.trim());
|
||||
}
|
||||
|
||||
export function loadStoredGatewayUrl() {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
return window.localStorage.getItem(GATEWAY_URL_KEY) ?? "";
|
||||
}
|
||||
|
||||
export function storeGatewayUrl(url: string) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
if (!url.trim()) {
|
||||
window.localStorage.removeItem(GATEWAY_URL_KEY);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(GATEWAY_URL_KEY, url.trim());
|
||||
}
|
||||
52
packages/dashboard-next/lib/url-state.ts
Normal file
52
packages/dashboard-next/lib/url-state.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
const TOKEN_PARAM = "token";
|
||||
const PASSWORD_PARAM = "password";
|
||||
const GATEWAY_URL_PARAM = "gatewayUrl";
|
||||
|
||||
export type BootstrapUrlState = {
|
||||
token: string | null;
|
||||
gatewayUrl: string | null;
|
||||
};
|
||||
|
||||
function parseHash(hash: string) {
|
||||
return new URLSearchParams(hash.startsWith("#") ? hash.slice(1) : hash);
|
||||
}
|
||||
|
||||
export function consumeBootstrapUrlState(): BootstrapUrlState {
|
||||
if (typeof window === "undefined") {
|
||||
return { token: null, gatewayUrl: null };
|
||||
}
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
const hashParams = parseHash(url.hash);
|
||||
|
||||
const tokenRaw = params.get(TOKEN_PARAM) ?? hashParams.get(TOKEN_PARAM);
|
||||
const gatewayUrlRaw = params.get(GATEWAY_URL_PARAM) ?? hashParams.get(GATEWAY_URL_PARAM);
|
||||
|
||||
const token = tokenRaw?.trim() || null;
|
||||
const gatewayUrl = gatewayUrlRaw?.trim() || null;
|
||||
|
||||
const hadSensitiveParam =
|
||||
tokenRaw !== null ||
|
||||
gatewayUrlRaw !== null ||
|
||||
params.has(PASSWORD_PARAM) ||
|
||||
hashParams.has(PASSWORD_PARAM);
|
||||
|
||||
if (hadSensitiveParam) {
|
||||
params.delete(TOKEN_PARAM);
|
||||
params.delete(PASSWORD_PARAM);
|
||||
params.delete(GATEWAY_URL_PARAM);
|
||||
hashParams.delete(TOKEN_PARAM);
|
||||
hashParams.delete(PASSWORD_PARAM);
|
||||
hashParams.delete(GATEWAY_URL_PARAM);
|
||||
|
||||
url.search = params.toString();
|
||||
const nextHash = hashParams.toString();
|
||||
url.hash = nextHash ? `#${nextHash}` : "";
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
|
||||
return { token, gatewayUrl };
|
||||
}
|
||||
6
packages/dashboard-next/next-env.d.ts
vendored
Normal file
6
packages/dashboard-next/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
21
packages/dashboard-next/next.config.mjs
Normal file
21
packages/dashboard-next/next.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
const basePathEnv = process.env.OPENCLAW_CONTROL_UI_BASE_PATH ?? "";
|
||||
|
||||
function normalizeBasePath(value) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed === "/") {
|
||||
return "";
|
||||
}
|
||||
const withLeading = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
||||
return withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
|
||||
}
|
||||
|
||||
const basePath = normalizeBasePath(basePathEnv);
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
basePath: basePath || undefined,
|
||||
transpilePackages: ["@openclaw/dashboard-gateway-client"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
24
packages/dashboard-next/package.json
Normal file
24
packages/dashboard-next/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@openclaw/dashboard-next",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openclaw/dashboard-gateway-client": "workspace:*",
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
31
packages/dashboard-next/tsconfig.json
Normal file
31
packages/dashboard-next/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
247
pnpm-lock.yaml
generated
247
pnpm-lock.yaml
generated
@@ -544,6 +544,36 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../..
|
||||
|
||||
packages/dashboard-gateway-client: {}
|
||||
|
||||
packages/dashboard-next:
|
||||
dependencies:
|
||||
'@openclaw/dashboard-gateway-client':
|
||||
specifier: workspace:*
|
||||
version: link:../dashboard-gateway-client
|
||||
next:
|
||||
specifier: ^16.0.0
|
||||
version: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4(react@19.2.4)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^25.3.0
|
||||
version: 25.3.0
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
'@types/react-dom':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.3(@types/react@19.2.14)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/moltbot:
|
||||
dependencies:
|
||||
openclaw:
|
||||
@@ -1651,6 +1681,57 @@ packages:
|
||||
'@napi-rs/wasm-runtime@1.1.1':
|
||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||
|
||||
'@next/env@16.1.6':
|
||||
resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.1.6':
|
||||
resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.1.6':
|
||||
resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.1.6':
|
||||
resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.6':
|
||||
resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.6':
|
||||
resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.6':
|
||||
resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.6':
|
||||
resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.1.6':
|
||||
resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@noble/ciphers@2.1.1':
|
||||
resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
@@ -2819,6 +2900,9 @@ packages:
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@swc/helpers@0.5.18':
|
||||
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
|
||||
|
||||
@@ -2959,6 +3043,14 @@ packages:
|
||||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/react-dom@19.2.3':
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^19.2.0
|
||||
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/request@2.48.13':
|
||||
resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==}
|
||||
|
||||
@@ -3287,6 +3379,11 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
baseline-browser-mapping@2.10.0:
|
||||
resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
basic-auth@2.0.1:
|
||||
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -3359,6 +3456,9 @@ packages:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
caniuse-lite@1.0.30001770:
|
||||
resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==}
|
||||
|
||||
caseless@0.12.0:
|
||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||
|
||||
@@ -3409,6 +3509,9 @@ packages:
|
||||
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
cliui@7.0.4:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
|
||||
@@ -3505,6 +3608,9 @@ packages:
|
||||
cssom@0.5.0:
|
||||
resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
curve25519-js@0.0.4:
|
||||
resolution: {integrity: sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==}
|
||||
|
||||
@@ -4565,6 +4671,27 @@ packages:
|
||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
next@16.1.6:
|
||||
resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
'@playwright/test': ^1.51.1
|
||||
babel-plugin-react-compiler: '*'
|
||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@playwright/test':
|
||||
optional: true
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-addon-api@8.5.0:
|
||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
@@ -4888,6 +5015,10 @@ packages:
|
||||
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
|
||||
engines: {node: '>=14.19.0'}
|
||||
|
||||
postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -5008,6 +5139,15 @@ packages:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
react-dom@19.2.4:
|
||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||
peerDependencies:
|
||||
react: ^19.2.4
|
||||
|
||||
react@19.2.4:
|
||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
@@ -5120,6 +5260,9 @@ packages:
|
||||
sanitize-html@2.17.1:
|
||||
resolution: {integrity: sha512-ehFCW+q1a4CSOWRAdX97BX/6/PDEkCqw7/0JXZAGQV57FQB3YOkTa/rrzHPeJ+Aghy4vZAFfWMYyfxIiB7F/gw==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
|
||||
@@ -5352,6 +5495,19 @@ packages:
|
||||
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '*'
|
||||
babel-plugin-macros: '*'
|
||||
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7190,6 +7346,32 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@next/env@16.1.6': {}
|
||||
|
||||
'@next/swc-darwin-arm64@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.1.6':
|
||||
optional: true
|
||||
|
||||
'@noble/ciphers@2.1.1': {}
|
||||
|
||||
'@noble/curves@2.0.1':
|
||||
@@ -8337,6 +8519,10 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@swc/helpers@0.5.18':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -8522,6 +8708,14 @@ snapshots:
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/react-dom@19.2.3(@types/react@19.2.14)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@types/react@19.2.14':
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/request@2.48.13':
|
||||
dependencies:
|
||||
'@types/caseless': 0.12.5
|
||||
@@ -8921,6 +9115,8 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.10.0: {}
|
||||
|
||||
basic-auth@2.0.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
@@ -9011,6 +9207,8 @@ snapshots:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
caniuse-lite@1.0.30001770: {}
|
||||
|
||||
caseless@0.12.0: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
@@ -9053,6 +9251,8 @@ snapshots:
|
||||
|
||||
cli-spinners@2.9.2: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
cliui@7.0.4:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -9155,6 +9355,8 @@ snapshots:
|
||||
|
||||
cssom@0.5.0: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
curve25519-js@0.0.4: {}
|
||||
|
||||
dashdash@1.14.1:
|
||||
@@ -10275,6 +10477,31 @@ snapshots:
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
||||
next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@next/env': 16.1.6
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.10.0
|
||||
caniuse-lite: 1.0.30001770
|
||||
postcss: 8.4.31
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
styled-jsx: 5.1.6(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.1.6
|
||||
'@next/swc-darwin-x64': 16.1.6
|
||||
'@next/swc-linux-arm64-gnu': 16.1.6
|
||||
'@next/swc-linux-arm64-musl': 16.1.6
|
||||
'@next/swc-linux-x64-gnu': 16.1.6
|
||||
'@next/swc-linux-x64-musl': 16.1.6
|
||||
'@next/swc-win32-arm64-msvc': 16.1.6
|
||||
'@next/swc-win32-x64-msvc': 16.1.6
|
||||
'@opentelemetry/api': 1.9.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-addon-api@8.5.0: {}
|
||||
|
||||
node-api-headers@1.8.0: {}
|
||||
@@ -10668,6 +10895,12 @@ snapshots:
|
||||
|
||||
pngjs@7.0.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@@ -10819,6 +11052,13 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
react-dom@19.2.4(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
scheduler: 0.27.0
|
||||
|
||||
react@19.2.4: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
@@ -10973,6 +11213,8 @@ snapshots:
|
||||
parse-srcset: 1.0.2
|
||||
postcss: 8.5.6
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
selderee@0.11.0:
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
@@ -11272,6 +11514,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
|
||||
styled-jsx@5.1.6(react@19.2.4):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.4
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
Reference in New Issue
Block a user