From e3f2326b7299885ac6839e5b77b6a3edab5de5b0 Mon Sep 17 00:00:00 2001 From: Azat Date: Mon, 2 Feb 2026 22:57:25 +0100 Subject: [PATCH] Initial homepage skill - dynamic dashboard --- SKILL.md | 46 +++++++ scripts/autorun.sh | 46 +++++++ scripts/run.sh | 312 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 404 insertions(+) create mode 100644 SKILL.md create mode 100644 scripts/autorun.sh create mode 100644 scripts/run.sh diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..c49eb57 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,46 @@ +--- +name: homepage +description: Dynamic dashboard - auto-discovers and displays all skills +metadata: + version: "1.0.0" + vibestack: + main: false +--- + +# Homepage Skill + +Dynamic dashboard that auto-discovers skills and shows their status. + +## Features + +- Auto-discovers skills from `/skills` directory +- Parses SKILL.md for name, description, ports +- Live health status checks +- Links to skill endpoints +- Minimal resource usage (~0 extra, just bash + socat) + +## Configuration + +| Variable | Description | Default | +|----------|-------------|---------| +| `HOMEPAGE_PORT` | Dashboard port | `3000` | +| `HOMEPAGE_DOMAIN` | Domain for Caddy auto-config | (none) | +| `HOMEPAGE_TITLE` | Page title | `VibeStack` | + +## Endpoints + +- `GET /` - Dashboard HTML +- `GET /api/skills` - JSON list of skills with status +- `GET /health` - Health check + +## What It Shows + +For each skill: +- Name and description +- Health status (up/down) +- Ports (parsed from SKILL.md or known defaults) +- Link to open (if applicable) + +## Customization + +The HTML template is in the skill's scripts. Claude can modify it to change the look. diff --git a/scripts/autorun.sh b/scripts/autorun.sh new file mode 100644 index 0000000..4614055 --- /dev/null +++ b/scripts/autorun.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +SKILLS_DIR="${SKILLS_DIR:-/skills}" + +# Install dependencies +install_deps() { + if command -v socat &>/dev/null && command -v jq &>/dev/null; then + return 0 + fi + + echo "Installing dependencies..." + apt-get update && apt-get install -y socat jq +} + +# Configure Caddy if present +configure_caddy() { + local caddy_dir="$SKILLS_DIR/caddy" + local homepage_port="${HOMEPAGE_PORT:-3000}" + local homepage_domain="${HOMEPAGE_DOMAIN:-}" + + if [ ! -d "$caddy_dir" ]; then + echo "Caddy not found - Homepage on port $homepage_port" + return 0 + fi + + echo "Caddy detected - configuring reverse proxy..." + mkdir -p "$caddy_dir/snippets.d" + + local snippet="$caddy_dir/snippets.d/homepage.caddy" + + if [ -n "$homepage_domain" ]; then + cat > "$snippet" << EOF +# Auto-generated by homepage skill +$homepage_domain { + reverse_proxy localhost:$homepage_port +} +EOF + echo "Caddy config: $homepage_domain -> localhost:$homepage_port" + fi +} + +install_deps +configure_caddy + +echo "Homepage setup complete" diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..3d65475 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,312 @@ +#!/bin/bash +set -e + +HOMEPAGE_PORT="${HOMEPAGE_PORT:-3000}" +HOMEPAGE_TITLE="${HOMEPAGE_TITLE:-VibeStack}" +SKILLS_DIR="${SKILLS_DIR:-/skills}" + +# Known ports for skills +declare -A KNOWN_PORTS=( + ["caddy"]="80" + ["ttyd"]="7681" + ["metrics"]="9090" + ["loki"]="3100" + ["duckdb"]="8432" + ["claude"]="8888" + ["openclaw"]="18789" + ["supervisor"]="9001" + ["homepage"]="3000" +) + +declare -A HEALTH_PORTS=( + ["caddy"]="2019" + ["ttyd"]="7681" + ["metrics"]="9090" + ["loki"]="3100" + ["duckdb"]="8432" + ["claude"]="8888" + ["openclaw"]="18789" + ["supervisor"]="9001" +) + +# Create handler script +create_handler() { + cat > /tmp/homepage_handler.sh << 'HANDLER' +#!/bin/bash + +HOMEPAGE_TITLE="${HOMEPAGE_TITLE:-VibeStack}" +SKILLS_DIR="${SKILLS_DIR:-/skills}" + +# Read request +read -r request_line +path=$(echo "$request_line" | cut -d' ' -f2) + +while read -r header; do + header=$(echo "$header" | tr -d '\r') + [ -z "$header" ] && break +done + +send_response() { + local status="$1" + local content_type="$2" + local body="$3" + local body_length=${#body} + + printf "HTTP/1.1 %s\r\n" "$status" + printf "Content-Type: %s\r\n" "$content_type" + printf "Content-Length: %d\r\n" "$body_length" + printf "Connection: close\r\n" + printf "\r\n" + printf "%s" "$body" +} + +# Get skill info +get_skills_json() { + local skills="[]" + + for skill_dir in "$SKILLS_DIR"/*/; do + local name=$(basename "$skill_dir") + local skill_md="$skill_dir/SKILL.md" + + [ ! -f "$skill_md" ] && continue + + # Parse SKILL.md + local frontmatter=$(sed -n '/^---$/,/^---$/p' "$skill_md" | sed '1d;$d') + local desc=$(echo "$frontmatter" | yq -r '.description // "No description"' 2>/dev/null || echo "") + local metrics_port=$(echo "$frontmatter" | yq -r '.metadata.vibestack."metrics-port" // empty' 2>/dev/null || echo "") + local is_main=$(echo "$frontmatter" | yq -r '.metadata.vibestack.main // false' 2>/dev/null || echo "false") + + # Get port + local port="" + case "$name" in + caddy) port="80" ;; + ttyd) port="7681" ;; + metrics) port="9090" ;; + loki) port="3100" ;; + duckdb) port="8432" ;; + claude) port="8888" ;; + openclaw) port="18789" ;; + supervisor) port="9001" ;; + homepage) port="3000" ;; + esac + + # Check health + local health_port="$port" + [ "$name" = "caddy" ] && health_port="2019" + + local status="unknown" + if [ -n "$health_port" ]; then + if curl -sf --max-time 1 "http://localhost:$health_port/health" >/dev/null 2>&1 || \ + curl -sf --max-time 1 "http://localhost:$health_port/" >/dev/null 2>&1; then + status="up" + else + status="down" + fi + fi + + skills=$(echo "$skills" | jq \ + --arg name "$name" \ + --arg desc "$desc" \ + --arg port "$port" \ + --arg status "$status" \ + --arg main "$is_main" \ + --arg metrics "$metrics_port" \ + '. + [{name:$name, description:$desc, port:$port, status:$status, main:($main=="true"), metrics_port:$metrics}]') + done + + echo "$skills" +} + +# Generate HTML +generate_html() { + local skills=$(get_skills_json) + + cat << HTML + + + + + + $HOMEPAGE_TITLE + + + +
+

$HOMEPAGE_TITLE

+
+
+ + + + +HTML +} + +# Route requests +case "$path" in + /health) + send_response "200 OK" "application/json" '{"status":"ok"}' + ;; + /api/skills) + skills=$(get_skills_json) + send_response "200 OK" "application/json" "$skills" + ;; + *) + html=$(generate_html) + send_response "200 OK" "text/html; charset=utf-8" "$html" + ;; +esac +HANDLER + + chmod +x /tmp/homepage_handler.sh + export HOMEPAGE_TITLE + export SKILLS_DIR +} + +# Serve +serve() { + echo "Starting Homepage on port $HOMEPAGE_PORT..." + exec socat TCP-LISTEN:$HOMEPAGE_PORT,reuseaddr,fork EXEC:"/tmp/homepage_handler.sh" +} + +create_handler +serve