Initial homepage skill - dynamic dashboard
This commit is contained in:
46
scripts/autorun.sh
Normal file
46
scripts/autorun.sh
Normal file
@@ -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"
|
||||
312
scripts/run.sh
Normal file
312
scripts/run.sh
Normal file
@@ -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
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>$HOMEPAGE_TITLE</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #f8fafc;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.card {
|
||||
background: #1e293b;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid #334155;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.card:hover { border-color: #3b82f6; }
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
}
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.status-up { background: #064e3b; color: #34d399; }
|
||||
.status-down { background: #7f1d1d; color: #f87171; }
|
||||
.status-unknown { background: #374151; color: #9ca3af; }
|
||||
.status::before {
|
||||
content: '';
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
.description {
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.port {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
background: #334155;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.link {
|
||||
font-size: 0.75rem;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
.link:hover { text-decoration: underline; }
|
||||
.main-badge {
|
||||
font-size: 0.625rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.refresh {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
border: none;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.refresh:hover { background: #475569; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>$HOMEPAGE_TITLE</h1>
|
||||
<div class="grid" id="skills"></div>
|
||||
</div>
|
||||
<button class="refresh" onclick="location.reload()">↻ Refresh</button>
|
||||
<script>
|
||||
const skills = $skills;
|
||||
const container = document.getElementById('skills');
|
||||
|
||||
skills.sort((a, b) => {
|
||||
if (a.main && !b.main) return -1;
|
||||
if (!a.main && b.main) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
skills.forEach(skill => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
|
||||
const statusClass = skill.status === 'up' ? 'status-up' :
|
||||
skill.status === 'down' ? 'status-down' : 'status-unknown';
|
||||
|
||||
let meta = '';
|
||||
if (skill.port) {
|
||||
meta += '<span class="port">:' + skill.port + '</span>';
|
||||
if (skill.status === 'up') {
|
||||
meta += ' <a class="link" href="http://localhost:' + skill.port + '" target="_blank">Open →</a>';
|
||||
}
|
||||
}
|
||||
|
||||
card.innerHTML =
|
||||
'<div class="card-header">' +
|
||||
'<span class="card-title">' + skill.name + (skill.main ? '<span class="main-badge">MAIN</span>' : '') + '</span>' +
|
||||
'<span class="status ' + statusClass + '">' + skill.status + '</span>' +
|
||||
'</div>' +
|
||||
'<p class="description">' + (skill.description || 'No description') + '</p>' +
|
||||
'<div class="meta">' + meta + '</div>';
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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
|
||||
Reference in New Issue
Block a user