diff --git a/bin/gstack-community-dashboard b/bin/gstack-community-dashboard index 5b7fc7ec..468dc1ea 100755 --- a/bin/gstack-community-dashboard +++ b/bin/gstack-community-dashboard @@ -70,7 +70,7 @@ echo "Top skills (last 7 days)" echo "────────────────────────" # Query telemetry_events, group by skill -EVENTS="$(query "telemetry_events" "select=skill,gstack_version&event_type=eq.skill_run&event_timestamp=gte.${WEEK_AGO}&limit=1000" 2>/dev/null || echo "[]")" +EVENTS="$(query "telemetry_events" "select=skill,gstack_version,session_id&event_type=eq.skill_run&event_timestamp=gte.${WEEK_AGO}&limit=1000" 2>/dev/null || echo "[]")" if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then echo "$EVENTS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -10 | while read -r COUNT SKILL; do @@ -109,5 +109,41 @@ else echo " No data yet" fi +# ─── Sessions (distinct session_id, works for all tiers) ──── +echo "Sessions (last 7 days)" +echo "──────────────────────" + +if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then + SESSION_COUNT="$(echo "$EVENTS" | grep -o '"session_id":"[^"]*"' | sort -u | wc -l | tr -d ' ')" + echo " ${SESSION_COUNT} unique sessions" +else + echo " No session data" +fi echo "" + +# ─── Skill recommendations ───────────────────────────────── +# Fetch top skills for recommendations +TOP_SKILLS="$(echo "$EVENTS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -3 | awk '{print $2}' | tr '\n' ',' | sed 's/,$//')" + +if [ -n "$TOP_SKILLS" ]; then + RECS="$(curl -sf --max-time 10 \ + "${SUPABASE_URL}/functions/v1/community-recommendations?skills=${TOP_SKILLS}" \ + -H "Authorization: Bearer ${ANON_KEY}" \ + 2>/dev/null || echo '{"recommendations":[]}')" + + REC_LIST="$(echo "$RECS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}')" + REC_REASONS="$(echo "$RECS" | grep -o '"reason":"[^"]*"' | awk -F'"' '{print $4}')" + + if [ -n "$REC_LIST" ]; then + echo "Skills you might like" + echo "─────────────────────" + paste <(echo "$REC_LIST") <(echo "$REC_REASONS") 2>/dev/null | while IFS=$'\t' read -r SKILL REASON; do + [ -z "$SKILL" ] && continue + printf " /%-20s %s\n" "$SKILL" "${REASON:-}" + done + echo "" + fi +fi + echo "For local analytics: gstack-analytics" +echo "For benchmarks: gstack-community-benchmarks" diff --git a/bin/gstack-telemetry-sync b/bin/gstack-telemetry-sync index 90e37243..d7ae2836 100755 --- a/bin/gstack-telemetry-sync +++ b/bin/gstack-telemetry-sync @@ -118,7 +118,26 @@ HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \ # ─── Update cursor on success (2xx) ───────────────────────── case "$HTTP_CODE" in 2*) NEW_CURSOR=$(( CURSOR + COUNT )) - echo "$NEW_CURSOR" > "$CURSOR_FILE" 2>/dev/null || true ;; + echo "$NEW_CURSOR" > "$CURSOR_FILE" 2>/dev/null || true + + # Ping update_checks (install base proxy) + GSTACK_VERSION="$(cat "$GSTACK_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "unknown")" + _OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + curl -sf --max-time 5 \ + -X POST "${ENDPOINT}/update_checks" \ + -H "Content-Type: application/json" \ + -H "apikey: ${ANON_KEY}" \ + -H "Authorization: Bearer ${ANON_KEY}" \ + -H "Prefer: return=minimal" \ + -d "{\"gstack_version\":\"$GSTACK_VERSION\",\"os\":\"$_OS\"}" \ + >/dev/null 2>&1 || true + + # Trigger community backup if community tier + BACKUP_CMD="$GSTACK_DIR/bin/gstack-community-backup" + if [ "$TIER" = "community" ] && [ -x "$BACKUP_CMD" ]; then + "$BACKUP_CMD" 2>/dev/null & + fi + ;; esac # Update rate limit marker diff --git a/supabase/functions/community-pulse/index.ts b/supabase/functions/community-pulse/index.ts index 23e30202..cd7539d8 100644 --- a/supabase/functions/community-pulse/index.ts +++ b/supabase/functions/community-pulse/index.ts @@ -15,21 +15,40 @@ Deno.serve(async () => { const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(); - // This week's active - const { count: thisWeek } = await supabase + // This week's active (update_checks) + const { count: thisWeekChecks } = await supabase .from("update_checks") .select("*", { count: "exact", head: true }) .gte("checked_at", weekAgo); // Last week's active (for change %) - const { count: lastWeek } = await supabase + const { count: lastWeekChecks } = await supabase .from("update_checks") .select("*", { count: "exact", head: true }) .gte("checked_at", twoWeeksAgo) .lt("checked_at", weekAgo); - const current = thisWeek ?? 0; - const previous = lastWeek ?? 0; + let current = thisWeekChecks ?? 0; + let previous = lastWeekChecks ?? 0; + + // Fallback: if update_checks is empty, count distinct sessions from telemetry_events + if (current === 0) { + const { data: thisWeekSessions } = await supabase + .from("telemetry_events") + .select("session_id") + .eq("event_type", "skill_run") + .gte("event_timestamp", weekAgo); + + const { data: lastWeekSessions } = await supabase + .from("telemetry_events") + .select("session_id") + .eq("event_type", "skill_run") + .gte("event_timestamp", twoWeeksAgo) + .lt("event_timestamp", weekAgo); + + current = new Set((thisWeekSessions ?? []).map((e: { session_id: string }) => e.session_id)).size; + previous = new Set((lastWeekSessions ?? []).map((e: { session_id: string }) => e.session_id)).size; + } const changePct = previous > 0 ? Math.round(((current - previous) / previous) * 100) : 0;