chore: sync pre-existing uncommitted work from migration era
These files accumulated between commit d4a3f2b (initial dashboard) and
today's fix work. Grouping them into one commit to avoid losing history
rather than attempting to backdate individual changes.
Contents:
• dashboard gateway routes: /api/gw/* + /api/status + /api/tiger/dispatch
• dashboard components: app-sidebar, cost-monitor, kanban-board, task-dialog
• dashboard hooks/lib: use-gateway, gateway client
• dashboard shadcn/ui: dialog, progress, select
• bridge routes: gateway (gateway proxy for bridge-side control)
• next.config.ts + package.json/lock updates
Future work should commit in smaller, topical units.
This commit is contained in:
parent
6621c6b28b
commit
1c04c9d5f1
17 changed files with 1004 additions and 264 deletions
54
bridge/src/routes/gateway.ts
Normal file
54
bridge/src/routes/gateway.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* gateway.ts — Proxy to OpenClaw Gateway inside Tiger container
|
||||
*
|
||||
* GET/POST /api/gateway/*
|
||||
* Forwards requests to the gateway running inside tiger-openclaw container
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Gateway URL - use Docker internal IP or container name
|
||||
// The Tiger container has IP 172.17.0.3 on docker0 network
|
||||
const GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL || "http://172.17.0.3:18789";
|
||||
|
||||
// Proxy all requests to the gateway inside the container
|
||||
router.all("/", async (req, res) => {
|
||||
try {
|
||||
const targetUrl = `${GATEWAY_URL}${req.originalUrl.replace("/api/gateway", "")}`;
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: req.method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(req.headers.authorization && {
|
||||
Authorization: req.headers.authorization,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
if (["POST", "PUT", "PATCH"].includes(req.method) && req.body) {
|
||||
fetchOptions.body = JSON.stringify(req.body);
|
||||
}
|
||||
|
||||
const response = await fetch(targetUrl, fetchOptions);
|
||||
const data = await response.json();
|
||||
|
||||
res.status(response.status).json(data);
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes("ECONNREFUSED")) {
|
||||
res.status(503).json({
|
||||
error: "Gateway not accessible",
|
||||
details: "The gateway is running inside the Tiger container and not reachable. Check Docker networking.",
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: "Failed to proxy to gateway",
|
||||
details: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
369
dashboard/package-lock.json
generated
369
dashboard/package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lightningcss": "^1.32.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
|
|
@ -3635,6 +3636,267 @@
|
|||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.30.2",
|
||||
"lightningcss-darwin-arm64": "1.30.2",
|
||||
"lightningcss-darwin-x64": "1.30.2",
|
||||
"lightningcss-freebsd-x64": "1.30.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||
"lightningcss-linux-x64-musl": "1.30.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||
|
|
@ -6091,7 +6353,6 @@
|
|||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
|
|
@ -8790,10 +9051,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"dev": true,
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
|
|
@ -8806,27 +9066,26 @@
|
|||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.30.2",
|
||||
"lightningcss-darwin-arm64": "1.30.2",
|
||||
"lightningcss-darwin-x64": "1.30.2",
|
||||
"lightningcss-freebsd-x64": "1.30.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||
"lightningcss-linux-x64-musl": "1.30.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8841,13 +9100,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8862,13 +9120,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8883,13 +9140,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8904,13 +9160,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8925,13 +9180,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8946,13 +9200,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8967,13 +9220,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -8988,13 +9240,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9009,13 +9260,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9030,13 +9280,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lightningcss": "^1.32.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getGateway } from "@/lib/gateway"
|
||||
|
||||
const BRIDGE_URL = process.env.TIGER_BRIDGE_URL || "http://localhost:3456"
|
||||
const BRIDGE_TOKEN = process.env.TIGER_BRIDGE_TOKEN || ""
|
||||
|
||||
// Map gateway-style methods to bridge endpoints
|
||||
const METHOD_MAP: Record<string, string> = {
|
||||
"status.canvas": "/tiger/status",
|
||||
"config.get": "/tiger/config",
|
||||
"config.set": "/tiger/config",
|
||||
}
|
||||
|
||||
// Proxy to the bridge instead of trying to reach the gateway directly
|
||||
// (gateway runs inside Tiger container - not accessible from dashboard)
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ method: string[] }> }
|
||||
|
|
@ -8,11 +19,21 @@ export async function POST(
|
|||
const { method: methodParts } = await params
|
||||
const method = methodParts.join(".")
|
||||
|
||||
// Map gateway method to bridge endpoint, or default to /tiger/status
|
||||
const bridgePath = METHOD_MAP[method] || `/tiger/${methodParts[0]}`
|
||||
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const gw = getGateway()
|
||||
const result = await gw.request(method, body)
|
||||
return NextResponse.json({ ok: true, data: result })
|
||||
const res = await fetch(`${BRIDGE_URL}${bridgePath}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${BRIDGE_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json({ ok: res.ok, data })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Gateway request failed"
|
||||
return NextResponse.json({ ok: false, error: message }, { status: 502 })
|
||||
|
|
@ -26,12 +47,16 @@ export async function GET(
|
|||
const { method: methodParts } = await params
|
||||
const method = methodParts.join(".")
|
||||
|
||||
const bridgePath = METHOD_MAP[method] || `/tiger/${methodParts[0]}`
|
||||
|
||||
try {
|
||||
const gw = getGateway()
|
||||
const result = await gw.request(method)
|
||||
return NextResponse.json({ ok: true, data: result })
|
||||
const res = await fetch(`${BRIDGE_URL}${bridgePath}`, {
|
||||
headers: { "Authorization": `Bearer ${BRIDGE_TOKEN}` },
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json({ ok: res.ok, data })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Gateway request failed"
|
||||
return NextResponse.json({ ok: false, error: message }, { status: 502 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +1,41 @@
|
|||
import { getGateway } from "@/lib/gateway"
|
||||
const BRIDGE_URL = process.env.TIGER_BRIDGE_URL || "http://localhost:3456"
|
||||
const BRIDGE_TOKEN = process.env.TIGER_BRIDGE_TOKEN || ""
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export async function GET() {
|
||||
const gw = getGateway()
|
||||
|
||||
// Ensure connected
|
||||
try {
|
||||
if (!gw.isConnected()) {
|
||||
await gw.connect()
|
||||
}
|
||||
} catch {
|
||||
return new Response("Gateway offline", { status: 502 })
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
let cleanupFn: (() => void) | null = null
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
const handler = ({ event, payload, seq }: { event: string; payload: unknown; seq: number }) => {
|
||||
if (event === "tick") return
|
||||
const data = JSON.stringify({ event, payload, seq })
|
||||
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
||||
}
|
||||
|
||||
gw.on("gateway-event", handler)
|
||||
|
||||
// Send initial connected message
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ event: "stream.connected", payload: { connected: true } })}\n\n`)
|
||||
encoder.encode(
|
||||
`data: ${JSON.stringify({ event: "stream.connected", payload: { connected: true } })}\n\n`
|
||||
)
|
||||
)
|
||||
|
||||
const keepalive = setInterval(() => {
|
||||
controller.enqueue(encoder.encode(`: keepalive\n\n`))
|
||||
}, 15000)
|
||||
// Poll tiger status instead of gateway directly
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetch(`${BRIDGE_URL}/tiger/status`, {
|
||||
headers: { "Authorization": `Bearer ${BRIDGE_TOKEN}` },
|
||||
})
|
||||
const data = await res.json()
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`data: ${JSON.stringify({ event: "health", payload: { status: data.status, ...data } })}\n\n`
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
controller.enqueue(encoder.encode(`: keepalive\n\n`))
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
const disconnectHandler = () => {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ event: "stream.disconnected", payload: { connected: false } })}\n\n`)
|
||||
)
|
||||
}
|
||||
gw.on("disconnected", disconnectHandler)
|
||||
|
||||
cleanupFn = () => {
|
||||
gw.off("gateway-event", handler)
|
||||
gw.off("disconnected", disconnectHandler)
|
||||
clearInterval(keepalive)
|
||||
}
|
||||
;(controller as any)._cleanup = () => clearInterval(interval)
|
||||
},
|
||||
cancel() {
|
||||
cleanupFn?.()
|
||||
cancel(controller: any) {
|
||||
controller?._cleanup?.()
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -60,4 +46,4 @@ export async function GET() {
|
|||
Connection: "keep-alive",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import os from "os"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { getGateway } from "@/lib/gateway"
|
||||
|
||||
const BRIDGE_URL = process.env.TIGER_BRIDGE_URL || "http://localhost:3456"
|
||||
const BRIDGE_TOKEN = process.env.TIGER_BRIDGE_TOKEN || ""
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
|
@ -10,147 +12,39 @@ export async function GET() {
|
|||
const totalMem = os.totalmem()
|
||||
const memUsage = Math.round(((totalMem - freeMem) / totalMem) * 100)
|
||||
|
||||
// Try gateway first for rich data
|
||||
try {
|
||||
const gw = getGateway()
|
||||
if (!gw.isConnected()) await gw.connect()
|
||||
|
||||
const [health, skills, cron, heartbeat, identity, models, config] = await Promise.allSettled([
|
||||
gw.request("health"),
|
||||
gw.request("skills.status"),
|
||||
gw.request("cron.list"),
|
||||
gw.request("last-heartbeat"),
|
||||
gw.request("agent.identity.get"),
|
||||
gw.request("models.list"),
|
||||
gw.request("config.get"),
|
||||
])
|
||||
|
||||
const healthData = health.status === "fulfilled" ? health.value as Record<string, unknown> : null
|
||||
const skillsData = skills.status === "fulfilled" ? skills.value as Record<string, unknown> : null
|
||||
const cronData = cron.status === "fulfilled" ? cron.value as unknown[] : null
|
||||
const heartbeatData = heartbeat.status === "fulfilled" ? heartbeat.value as Record<string, unknown> : null
|
||||
const identityData = identity.status === "fulfilled" ? identity.value as Record<string, unknown> : null
|
||||
const modelsData = models.status === "fulfilled" ? models.value as Record<string, unknown> : null
|
||||
const configData = config.status === "fulfilled" ? config.value as Record<string, unknown> : null
|
||||
|
||||
const skillsList = (skillsData?.skills || skillsData?.installed || []) as unknown[]
|
||||
const cronList = Array.isArray(cronData) ? cronData : ((cronData as Record<string, unknown> | null)?.jobs as unknown[] | undefined) || []
|
||||
|
||||
// Extract current model from config - try multiple response shapes
|
||||
// Gateway config.get may return: raw config, { config: ... }, or nested differently
|
||||
const rawConfig = (configData?.config as Record<string, unknown>) || configData
|
||||
const agentsConfig = (rawConfig?.agents as Record<string, unknown>) || undefined
|
||||
const defaultsConfig = (agentsConfig?.defaults as Record<string, unknown>) || undefined
|
||||
const modelConfig = (defaultsConfig?.model as Record<string, unknown>) || undefined
|
||||
let currentModel = (modelConfig?.primary as string) || null
|
||||
let fallbackModels = ((modelConfig?.fallbacks || []) as string[])
|
||||
|
||||
// Fallback: read directly from config file if gateway didn't return model info
|
||||
if (!currentModel) {
|
||||
try {
|
||||
const configFilePath = path.join(os.homedir(), ".clawdbot", "clawdbot.json")
|
||||
const fileConfig = JSON.parse(fs.readFileSync(configFilePath, "utf-8"))
|
||||
currentModel = fileConfig?.agents?.defaults?.model?.primary || null
|
||||
if (!fallbackModels.length) {
|
||||
fallbackModels = fileConfig?.agents?.defaults?.model?.fallbacks || []
|
||||
}
|
||||
} catch {
|
||||
// config file not readable
|
||||
}
|
||||
}
|
||||
|
||||
// Also extract the raw config hash for conflict-safe patching
|
||||
const configHash = configData?._hash || configData?.hash || null
|
||||
|
||||
// Extract models list: { models: [{id, name, provider, contextWindow, reasoning, input}] }
|
||||
const modelsList = (modelsData?.models || []) as unknown[]
|
||||
|
||||
// Read HEARTBEAT.md for heartbeat task info
|
||||
let heartbeatContent: string | null = null
|
||||
try {
|
||||
const workspace = (configData?.agents as Record<string, unknown> | undefined)?.defaults as Record<string, unknown> | undefined
|
||||
const wsPath = (workspace?.workspace as string) || "/Users/manohar_air/clawd"
|
||||
const hbPath = path.join(wsPath, "HEARTBEAT.md")
|
||||
heartbeatContent = fs.readFileSync(hbPath, "utf-8").trim()
|
||||
} catch {
|
||||
// HEARTBEAT.md not found
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
status: "online",
|
||||
gateway: true,
|
||||
system: {
|
||||
memoryUsage: memUsage,
|
||||
uptime: os.uptime(),
|
||||
platform: os.platform(),
|
||||
},
|
||||
agent: {
|
||||
name: identityData?.name || "Tarzan",
|
||||
vibe: identityData?.vibe || "",
|
||||
emoji: identityData?.emoji || "",
|
||||
skills: skillsList.length,
|
||||
cronJobs: cronList.filter((j: unknown) => (j as Record<string, unknown>)?.enabled).length,
|
||||
cronTotal: cronList.length,
|
||||
lastHeartbeat: heartbeatData?.timestamp || heartbeatData?.lastChecked || null,
|
||||
heartbeatContent,
|
||||
currentModel,
|
||||
fallbackModels,
|
||||
},
|
||||
models: modelsList,
|
||||
configHash,
|
||||
health: healthData,
|
||||
})
|
||||
} catch {
|
||||
// Gateway not available - fall back to HTTP probe
|
||||
}
|
||||
|
||||
// Fallback: HTTP probe + file reads
|
||||
// Use bridge's /tiger/status instead of gateway directly
|
||||
// Gateway runs inside Tiger container and is not directly accessible
|
||||
let agentStatus = "offline"
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1000)
|
||||
const response = await fetch("http://127.0.0.1:18789/__clawdbot__/canvas/", {
|
||||
signal: controller.signal,
|
||||
cache: "no-store",
|
||||
})
|
||||
clearTimeout(timeoutId)
|
||||
if (response.ok) agentStatus = "online"
|
||||
} catch {
|
||||
// offline
|
||||
}
|
||||
let gatewayConnected = false
|
||||
let tigerStatus: any = null
|
||||
|
||||
// Even without gateway, try to read config file for model info
|
||||
let fallbackModel: string | null = null
|
||||
let fallbackFallbacks: string[] = []
|
||||
let fallbackHeartbeat: string | null = null
|
||||
try {
|
||||
const configFilePath = path.join(os.homedir(), ".clawdbot", "clawdbot.json")
|
||||
const fileConfig = JSON.parse(fs.readFileSync(configFilePath, "utf-8"))
|
||||
fallbackModel = fileConfig?.agents?.defaults?.model?.primary || null
|
||||
fallbackFallbacks = fileConfig?.agents?.defaults?.model?.fallbacks || []
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
fallbackHeartbeat = fs.readFileSync(path.join("/Users/manohar_air/clawd", "HEARTBEAT.md"), "utf-8").trim()
|
||||
} catch { /* ignore */ }
|
||||
const res = await fetch(`${BRIDGE_URL}/tiger/status`, {
|
||||
headers: { "Authorization": `Bearer ${BRIDGE_TOKEN}` },
|
||||
})
|
||||
if (res.ok) {
|
||||
tigerStatus = await res.json()
|
||||
agentStatus = tigerStatus?.status === "online" ? "online" : "degraded"
|
||||
gatewayConnected = tigerStatus?.status === "online"
|
||||
}
|
||||
} catch { /* offline */ }
|
||||
|
||||
return NextResponse.json({
|
||||
status: agentStatus,
|
||||
gateway: false,
|
||||
system: {
|
||||
memoryUsage: memUsage,
|
||||
uptime: os.uptime(),
|
||||
platform: os.platform(),
|
||||
},
|
||||
gateway: gatewayConnected,
|
||||
system: { memoryUsage: memUsage, uptime: os.uptime(), platform: os.platform() },
|
||||
agent: {
|
||||
name: "Tiger",
|
||||
skills: 0,
|
||||
cronJobs: 0,
|
||||
lastHeartbeat: null,
|
||||
heartbeatContent: fallbackHeartbeat,
|
||||
currentModel: fallbackModel,
|
||||
fallbackModels: fallbackFallbacks,
|
||||
lastHeartbeat: tigerStatus?.agent?.heartbeat,
|
||||
currentModel: tigerStatus?.agent?.currentModel,
|
||||
fallbackModels: tigerStatus?.agent?.fallbackModels || [],
|
||||
container: tigerStatus?.container?.status,
|
||||
memoryUsagePct: tigerStatus?.system?.memoryUsagePct,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to fetch status" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,17 +21,3 @@ export async function POST(request: Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// GET /api/tiger/dispatch/status/:taskId
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ taskId: string }> }
|
||||
) {
|
||||
const { taskId } = await params;
|
||||
try {
|
||||
const result = await bridgePost(`/tiger/dispatch/status/${taskId}`, {});
|
||||
return NextResponse.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
return NextResponse.json({ ok: false, error: message }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
Bot,
|
||||
Settings2,
|
||||
LayoutDashboard,
|
||||
MessageSquare,
|
||||
ScrollText,
|
||||
CheckSquare,
|
||||
DollarSign,
|
||||
|
|
@ -31,6 +32,11 @@ const navMain = [
|
|||
url: "/",
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: "Chat",
|
||||
url: "/chat",
|
||||
icon: MessageSquare,
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
url: "/projects",
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export function CostMonitor() {
|
|||
<YAxis tick={{fontSize: 10}} stroke="#6b7280" tickFormatter={(v) => `$${v.toFixed(2)}`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: 'none', borderRadius: '6px' }}
|
||||
formatter={(value: number) => [`$${value.toFixed(4)}`, 'Cost']}
|
||||
formatter={(value) => [`$${Number(value).toFixed(4)}`, 'Cost']}
|
||||
/>
|
||||
<Area type="monotone" dataKey="cost" stroke="#10b981" fillOpacity={1} fill="url(#colorCost)" />
|
||||
</AreaChart>
|
||||
|
|
@ -217,9 +217,9 @@ export function CostMonitor() {
|
|||
<YAxis type="category" dataKey="name" tick={{fontSize: 10}} stroke="#6b7280" width={100} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: 'none', borderRadius: '6px' }}
|
||||
formatter={(value: number, _name: string, props: any) => {
|
||||
formatter={(value, _name, props) => {
|
||||
const model = props?.payload?.fullModel || ''
|
||||
return [`$${value.toFixed(4)}`, model]
|
||||
return [`$${Number(value).toFixed(4)}`, model]
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="cost" fill="#8b5cf6" radius={[0, 4, 4, 0]}>
|
||||
|
|
|
|||
|
|
@ -443,13 +443,13 @@ export function KanbanBoard() {
|
|||
id: editingTask.id,
|
||||
title: editingTask.title,
|
||||
description: editingTask.description,
|
||||
status: editingTask.status,
|
||||
status: editingTask.status as string,
|
||||
priority: editingTask.priority,
|
||||
assigned_agent: editingTask.assigned_agent,
|
||||
progress: editingTask.progress,
|
||||
tags: editingTask.tags,
|
||||
} : null}
|
||||
onSubmit={editingTask ? handleUpdateTask : handleAddTask}
|
||||
onSubmit={editingTask ? handleUpdateTask as any : handleAddTask}
|
||||
onDelete={editingTask ? () => handleDeleteTask(editingTask.id) : undefined}
|
||||
onRun={editingTask ? () => handleRunTask(editingTask.id) : undefined}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ interface TaskDialogProps {
|
|||
onSubmit: (data: {
|
||||
title: string
|
||||
description?: string
|
||||
status: string
|
||||
priority: string
|
||||
assigned_agent?: string
|
||||
status?: string
|
||||
priority?: string
|
||||
assigned_agent?: string | null
|
||||
progress?: number
|
||||
tags?: string[]
|
||||
due_date?: string
|
||||
|
|
|
|||
158
dashboard/src/components/ui/dialog.tsx
Normal file
158
dashboard/src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
31
dashboard/src/components/ui/progress.tsx
Normal file
31
dashboard/src/components/ui/progress.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Progress as ProgressPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
190
dashboard/src/components/ui/select.tsx
Normal file
190
dashboard/src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import { Select as SelectPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
90
dashboard/src/hooks/use-gateway.ts
Normal file
90
dashboard/src/hooks/use-gateway.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"use client"
|
||||
|
||||
/**
|
||||
* use-gateway.ts — Client-side hooks for OpenClaw gateway interaction
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from "react"
|
||||
|
||||
const GW_API_PREFIX = "/api/gw"
|
||||
|
||||
/**
|
||||
* useGatewayRequest — make RPC-style calls to the gateway via /api/gw/[...method]
|
||||
* Usage: request("cron.list") => POST /api/gw/cron/list
|
||||
* request("cron.run", { id: "abc" }) => POST /api/gw/cron/run with body
|
||||
*/
|
||||
export function useGatewayRequest() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const request = useCallback(async (method: string, body?: Record<string, unknown>) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const path = method.replace(/\./g, "/")
|
||||
const res = await fetch(`${GW_API_PREFIX}/${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body || {}),
|
||||
})
|
||||
if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`)
|
||||
const json = await res.json()
|
||||
return json.data ?? json
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
return null
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { request, loading, error }
|
||||
}
|
||||
|
||||
/**
|
||||
* useGatewayEvents — SSE event stream from /api/gw/stream
|
||||
*/
|
||||
export function useGatewayEvents(path?: string, options?: { enabled?: boolean }) {
|
||||
const [events, setEvents] = useState<any[]>([])
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const esRef = useRef<EventSource | null>(null)
|
||||
const enabled = options?.enabled ?? true
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
|
||||
const url = path || "/api/gw/stream"
|
||||
const es = new EventSource(url)
|
||||
esRef.current = es
|
||||
|
||||
es.onopen = () => {
|
||||
setConnected(true)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data)
|
||||
setEvents((prev) => [...prev.slice(-500), data])
|
||||
} catch {
|
||||
setEvents((prev) => [...prev.slice(-500), { raw: e.data }])
|
||||
}
|
||||
}
|
||||
|
||||
es.onerror = () => {
|
||||
setConnected(false)
|
||||
setError("Connection lost")
|
||||
}
|
||||
|
||||
return () => {
|
||||
es.close()
|
||||
esRef.current = null
|
||||
setConnected(false)
|
||||
}
|
||||
}, [path, enabled])
|
||||
|
||||
const clear = useCallback(() => setEvents([]), [])
|
||||
|
||||
return { events, connected, error, clear }
|
||||
}
|
||||
68
dashboard/src/lib/gateway.ts
Normal file
68
dashboard/src/lib/gateway.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* gateway.ts — OpenClaw Gateway client for server-side API routes
|
||||
*
|
||||
* v3: Routes through Tiger Bridge proxy because the gateway runs inside
|
||||
* the Tiger Docker container and is not directly accessible from Dokploy.
|
||||
*/
|
||||
|
||||
const BRIDGE_URL = process.env.TIGER_BRIDGE_URL || "http://localhost:3456"
|
||||
const BRIDGE_TOKEN = process.env.TIGER_BRIDGE_TOKEN || ""
|
||||
const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || ""
|
||||
|
||||
interface GatewayOptions {
|
||||
method?: string
|
||||
body?: any
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export function getGateway() {
|
||||
// Use the bridge proxy which can access the gateway inside the container
|
||||
const gatewayBase = `${BRIDGE_URL}/api/gateway`
|
||||
|
||||
return {
|
||||
url: gatewayBase,
|
||||
token: GATEWAY_TOKEN,
|
||||
|
||||
async request(path: string, opts: GatewayOptions = {}) {
|
||||
const { method = "GET", body, timeout = 30000 } = opts
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${gatewayBase}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${BRIDGE_TOKEN}`,
|
||||
...(GATEWAY_TOKEN ? { "X-Gateway-Token": GATEWAY_TOKEN } : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Gateway ${res.status}: ${res.statusText}`)
|
||||
}
|
||||
|
||||
return res
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
},
|
||||
|
||||
async json(path: string, opts: GatewayOptions = {}) {
|
||||
const res = await this.request(path, opts)
|
||||
return res.json()
|
||||
},
|
||||
|
||||
async text(path: string, opts: GatewayOptions = {}) {
|
||||
const res = await this.request(path, opts)
|
||||
return res.text()
|
||||
},
|
||||
|
||||
streamUrl(path: string) {
|
||||
const sep = path.includes("?") ? "&" : "?"
|
||||
return `${gatewayBase}${path}${GATEWAY_TOKEN ? `${sep}token=${GATEWAY_TOKEN}` : ""}`
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue