commit bc2f2e89ed72f8727eeefef271dcac9694b77cc7 Author: Azat Date: Tue Feb 3 00:46:40 2026 +0100 Initial fleet skill diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..d3c5c7d --- /dev/null +++ b/SKILL.md @@ -0,0 +1,34 @@ +--- +name: fleet +description: Multi-machine dashboard for VibeStack fleet management +metadata: + version: "1.0.0" + vibestack: + main: false + requires: + - fleet-api +--- + +# Fleet Dashboard + +Web UI for managing multiple VibeStack machines. Displays registered machines in a sidebar and shows their dashboards in iframes. + +## Features + +- Machine list with status indicators +- Iframe embedding for machine dashboards +- Real-time status updates (polling every 10s) +- Overview page with machine cards + +## Configuration + +- `FLEET_UI_PORT` - UI server port (default: 3000) +- `FLEET_API_URL` - Fleet API URL (default: http://localhost:3001) + +## Usage + +1. Deploy fleet-api on the host machine +2. Deploy fleet UI on the host machine +3. Deploy fleet-client on each worker machine +4. Workers register with fleet-api +5. View all machines in the fleet dashboard diff --git a/scripts/autorun.sh b/scripts/autorun.sh new file mode 100644 index 0000000..54e6cce --- /dev/null +++ b/scripts/autorun.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +SKILL_DIR="$(dirname "$(dirname "$0")")" +UI_DIR="$SKILL_DIR/ui" + +echo "=== Fleet Dashboard Setup ===" + +# Check for Node.js +if ! command -v node &>/dev/null; then + echo "Installing Node.js..." + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y nodejs +fi + +echo "Node.js version: $(node --version)" +echo "npm version: $(npm --version)" + +# Install UI dependencies +cd "$UI_DIR" +if [ ! -d "node_modules" ]; then + echo "Installing UI dependencies..." + npm install +fi + +# Build UI +echo "Building UI..." +npm run build + +echo "Fleet Dashboard setup complete" diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..3af5ba9 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +FLEET_UI_PORT="${FLEET_UI_PORT:-3000}" +SKILL_DIR="$(dirname "$(dirname "$0")")" +UI_DIR="$SKILL_DIR/ui" + +export FLEET_UI_PORT +export FLEET_API_URL="${FLEET_API_URL:-http://localhost:3001}" + +echo "Starting Fleet Dashboard on port $FLEET_UI_PORT..." +echo "Fleet API: $FLEET_API_URL" + +cd "$UI_DIR" + +if [ "${NODE_ENV:-production}" = "development" ]; then + echo "Running in development mode..." + exec npm run dev -- --port "$FLEET_UI_PORT" --host +else + echo "Running in production mode..." + exec npm run preview -- --port "$FLEET_UI_PORT" --host +fi diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..dab7b73 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Fleet Dashboard + + +
+ + + diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..ed13f8c --- /dev/null +++ b/ui/package.json @@ -0,0 +1,29 @@ +{ + "name": "vibestack-fleet", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint src --ext ts,tsx" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "@tanstack/react-query": "^5.17.0", + "lucide-react": "^0.303.0", + "clsx": "^2.1.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.0", + "vite": "^5.0.0" + } +} diff --git a/ui/postcss.config.js b/ui/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/ui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..eafcc3a --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,15 @@ +import { Routes, Route } from 'react-router-dom' +import { Layout } from './components/Layout' +import { Overview } from './pages/Overview' +import { Machine } from './pages/Machine' + +export default function App() { + return ( + + }> + } /> + } /> + + + ) +} diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx new file mode 100644 index 0000000..68542de --- /dev/null +++ b/ui/src/components/Layout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from 'react-router-dom' +import { Sidebar } from './Sidebar' + +export function Layout() { + return ( +
+ +
+ +
+
+ ) +} diff --git a/ui/src/components/MachineCard.tsx b/ui/src/components/MachineCard.tsx new file mode 100644 index 0000000..050a1b0 --- /dev/null +++ b/ui/src/components/MachineCard.tsx @@ -0,0 +1,75 @@ +import { Link } from 'react-router-dom' +import { Server, Clock, Blocks } from 'lucide-react' +import { clsx } from 'clsx' +import { Machine, timeSince } from '../lib/api' +import { StatusBadge } from './StatusBadge' + +interface MachineCardProps { + machine: Machine +} + +export function MachineCard({ machine }: MachineCardProps) { + return ( + +
+
+
+ +
+
+

{machine.name}

+

{machine.id.slice(0, 8)}...

+
+
+ +
+ +
+
+ + Last seen: {timeSince(machine.lastSeen)} +
+ +
+ + {machine.skills.length} skills +
+ + {machine.skills.length > 0 && ( +
+ {machine.skills.slice(0, 4).map((skill) => ( + + {skill} + + ))} + {machine.skills.length > 4 && ( + + +{machine.skills.length - 4} more + + )} +
+ )} +
+ +
+ {machine.url} +
+ + ) +} diff --git a/ui/src/components/MachineFrame.tsx b/ui/src/components/MachineFrame.tsx new file mode 100644 index 0000000..93f19bf --- /dev/null +++ b/ui/src/components/MachineFrame.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react' +import { ExternalLink, RefreshCw, AlertCircle } from 'lucide-react' +import { Machine } from '../lib/api' +import { StatusBadge } from './StatusBadge' + +interface MachineFrameProps { + machine: Machine +} + +export function MachineFrame({ machine }: MachineFrameProps) { + const [isLoading, setIsLoading] = useState(true) + const [hasError, setHasError] = useState(false) + + const handleLoad = () => { + setIsLoading(false) + setHasError(false) + } + + const handleError = () => { + setIsLoading(false) + setHasError(true) + } + + const refresh = () => { + setIsLoading(true) + setHasError(false) + // Force iframe reload by updating key + const iframe = document.getElementById('machine-frame') as HTMLIFrameElement + if (iframe) { + iframe.src = machine.url + } + } + + return ( +
+
+
+

{machine.name}

+ +
+ +
+ + + + +
+
+ +
+ {isLoading && ( +
+
+ + Loading dashboard... +
+
+ )} + + {hasError && ( +
+
+ +

Failed to load dashboard

+

{machine.url}

+ +
+
+ )} + + {machine.status === 'unknown' && ( +
+ Machine may be offline - last heartbeat was over 60 seconds ago +
+ )} + +