Merge pull request 'fix: feat: live factory metrics dashboard on disinto.ai (#395)' (#496) from fix/issue-395 into main
This commit is contained in:
commit
038581e555
3 changed files with 763 additions and 0 deletions
234
site/collect-metrics.sh
Normal file
234
site/collect-metrics.sh
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# collect-metrics.sh — Collect factory metrics and write JSON for the dashboard
|
||||||
|
#
|
||||||
|
# Queries Codeberg API for PR/issue stats across all managed projects,
|
||||||
|
# counts vault decisions, and checks CI pass rates. Writes a JSON snapshot
|
||||||
|
# to the live site directory so the dashboard can fetch it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash site/collect-metrics.sh
|
||||||
|
#
|
||||||
|
# Cron: 0 */6 * * * cd /home/debian/dark-factory && bash site/collect-metrics.sh
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# shellcheck source=../lib/env.sh
|
||||||
|
source "$FACTORY_ROOT/lib/env.sh"
|
||||||
|
# shellcheck source=../lib/ci-helpers.sh
|
||||||
|
source "$FACTORY_ROOT/lib/ci-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
LOGFILE="${FACTORY_ROOT}/site/collect-metrics.log"
|
||||||
|
log() {
|
||||||
|
printf '[%s] collect-metrics: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Output path: write to live site root if deployed, else to site/data/
|
||||||
|
SITE_ROOT="${DISINTO_SITE_ROOT:-/home/debian/disinto-site}"
|
||||||
|
if [ -d "$SITE_ROOT" ] || [ -L "$SITE_ROOT" ]; then
|
||||||
|
OUTPUT_DIR="$(readlink -f "$SITE_ROOT")/data"
|
||||||
|
else
|
||||||
|
OUTPUT_DIR="${SCRIPT_DIR}/data"
|
||||||
|
fi
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
WEEK_AGO=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
|
||||||
|
MONTH_AGO=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# ── Per-project metrics ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
collect_project_metrics() {
|
||||||
|
local project_toml="$1"
|
||||||
|
local repo repo_name
|
||||||
|
|
||||||
|
repo=$(grep '^repo ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//')
|
||||||
|
repo_name=$(grep '^name ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//')
|
||||||
|
local api_base="https://codeberg.org/api/v1/repos/${repo}"
|
||||||
|
|
||||||
|
# PRs merged (all time via state=closed + merged marker)
|
||||||
|
local prs_merged_week=0 prs_merged_month=0 prs_merged_total=0
|
||||||
|
local closed_prs
|
||||||
|
closed_prs=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api_base}/pulls?state=closed&sort=updated&limit=50" 2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
prs_merged_total=$(printf '%s' "$closed_prs" | jq '[.[] | select(.merged)] | length' 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ -n "$WEEK_AGO" ]; then
|
||||||
|
prs_merged_week=$(printf '%s' "$closed_prs" | \
|
||||||
|
jq --arg since "$WEEK_AGO" '[.[] | select(.merged and .merged_at >= $since)] | length' 2>/dev/null || echo 0)
|
||||||
|
fi
|
||||||
|
if [ -n "$MONTH_AGO" ]; then
|
||||||
|
prs_merged_month=$(printf '%s' "$closed_prs" | \
|
||||||
|
jq --arg since "$MONTH_AGO" '[.[] | select(.merged and .merged_at >= $since)] | length' 2>/dev/null || echo 0)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Issues closed
|
||||||
|
local issues_closed_week=0 issues_closed_month=0
|
||||||
|
local closed_issues
|
||||||
|
closed_issues=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api_base}/issues?state=closed&sort=updated&type=issues&limit=50" 2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
if [ -n "$WEEK_AGO" ]; then
|
||||||
|
issues_closed_week=$(printf '%s' "$closed_issues" | \
|
||||||
|
jq --arg since "$WEEK_AGO" '[.[] | select(.closed_at >= $since)] | length' 2>/dev/null || echo 0)
|
||||||
|
fi
|
||||||
|
if [ -n "$MONTH_AGO" ]; then
|
||||||
|
issues_closed_month=$(printf '%s' "$closed_issues" | \
|
||||||
|
jq --arg since "$MONTH_AGO" '[.[] | select(.closed_at >= $since)] | length' 2>/dev/null || echo 0)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local total_closed_header
|
||||||
|
total_closed_header=$(curl -sf -I -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api_base}/issues?state=closed&type=issues&limit=1" 2>/dev/null | grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}' || echo "0")
|
||||||
|
local issues_closed_total="${total_closed_header:-0}"
|
||||||
|
|
||||||
|
# Open issues by label
|
||||||
|
local backlog_count in_progress_count blocked_count
|
||||||
|
backlog_count=$(curl -sf -I -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api_base}/issues?state=open&labels=backlog&type=issues&limit=1" 2>/dev/null | \
|
||||||
|
grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}' || echo "0")
|
||||||
|
in_progress_count=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api_base}/issues?state=open&labels=in-progress&type=issues&limit=50" 2>/dev/null | \
|
||||||
|
jq 'length' 2>/dev/null || echo 0)
|
||||||
|
blocked_count=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api_base}/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null | \
|
||||||
|
jq 'length' 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
jq -nc \
|
||||||
|
--arg name "$repo_name" \
|
||||||
|
--arg repo "$repo" \
|
||||||
|
--argjson prs_week "$prs_merged_week" \
|
||||||
|
--argjson prs_month "$prs_merged_month" \
|
||||||
|
--argjson prs_total "${prs_merged_total:-0}" \
|
||||||
|
--argjson issues_week "$issues_closed_week" \
|
||||||
|
--argjson issues_month "$issues_closed_month" \
|
||||||
|
--argjson issues_total "${issues_closed_total:-0}" \
|
||||||
|
--argjson backlog "${backlog_count:-0}" \
|
||||||
|
--argjson in_progress "${in_progress_count:-0}" \
|
||||||
|
--argjson blocked "${blocked_count:-0}" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
repo: $repo,
|
||||||
|
prs_merged: { week: $prs_week, month: $prs_month, total: $prs_total },
|
||||||
|
issues_closed: { week: $issues_week, month: $issues_month, total: $issues_total },
|
||||||
|
backlog: { queued: $backlog, in_progress: $in_progress, blocked: $blocked }
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vault decisions ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
collect_vault_metrics() {
|
||||||
|
local vault_dir="${FACTORY_ROOT}/vault"
|
||||||
|
local approved=0 rejected=0 escalated=0 pending=0 fired=0
|
||||||
|
|
||||||
|
[ -d "$vault_dir/fired" ] && fired=$(find "$vault_dir/fired" -name '*.json' 2>/dev/null | wc -l)
|
||||||
|
[ -d "$vault_dir/approved" ] && approved=$(find "$vault_dir/approved" -name '*.json' 2>/dev/null | wc -l)
|
||||||
|
[ -d "$vault_dir/rejected" ] && rejected=$(find "$vault_dir/rejected" -name '*.json' 2>/dev/null | wc -l)
|
||||||
|
[ -d "$vault_dir/pending" ] && {
|
||||||
|
pending=$(find "$vault_dir/pending" -name '*.json' 2>/dev/null | wc -l)
|
||||||
|
escalated=$(find "$vault_dir/pending" -name '*.json' -exec grep -l '"escalated"' {} + 2>/dev/null | wc -l)
|
||||||
|
pending=$((pending - escalated))
|
||||||
|
}
|
||||||
|
|
||||||
|
jq -nc \
|
||||||
|
--argjson approved "$((approved + fired))" \
|
||||||
|
--argjson escalated "$escalated" \
|
||||||
|
--argjson rejected "$rejected" \
|
||||||
|
--argjson pending "$pending" \
|
||||||
|
'{ approved: $approved, escalated: $escalated, rejected: $rejected, pending: $pending }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── CI pass rate ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
collect_ci_metrics() {
|
||||||
|
local total=0 passed=0 failed=0 rate=0
|
||||||
|
|
||||||
|
# Query Woodpecker DB for last 30 days across all repos
|
||||||
|
local ci_stats
|
||||||
|
ci_stats=$(wpdb -A -c "
|
||||||
|
SELECT status, count(*)
|
||||||
|
FROM pipelines
|
||||||
|
WHERE finished > 0
|
||||||
|
AND to_timestamp(finished) > now() - interval '30 days'
|
||||||
|
GROUP BY status;" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$ci_stats" ]; then
|
||||||
|
passed=$(echo "$ci_stats" | awk '$1 == "success" {print $3}' | tr -d ' ' || echo 0)
|
||||||
|
failed=$(echo "$ci_stats" | awk '$1 == "failure" {print $3}' | tr -d ' ' || echo 0)
|
||||||
|
passed=${passed:-0}
|
||||||
|
failed=${failed:-0}
|
||||||
|
total=$((passed + failed))
|
||||||
|
if [ "$total" -gt 0 ]; then
|
||||||
|
rate=$(( (passed * 100) / total ))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -nc \
|
||||||
|
--argjson total "$total" \
|
||||||
|
--argjson passed "$passed" \
|
||||||
|
--argjson failed "$failed" \
|
||||||
|
--argjson rate "$rate" \
|
||||||
|
'{ total: $total, passed: $passed, failed: $failed, pass_rate: $rate }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Agent activity ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
collect_agent_metrics() {
|
||||||
|
local active_sessions=0
|
||||||
|
active_sessions=$(tmux list-sessions 2>/dev/null | wc -l || echo 0)
|
||||||
|
|
||||||
|
local agents='[]'
|
||||||
|
local agent_name log_path age_min last_active
|
||||||
|
for log_entry in dev/dev-agent.log review/review.log gardener/gardener.log \
|
||||||
|
planner/planner.log predictor/predictor.log supervisor/supervisor.log \
|
||||||
|
action/action.log vault/vault.log; do
|
||||||
|
agent_name=$(basename "$(dirname "$log_entry")")
|
||||||
|
log_path="${FACTORY_ROOT}/${log_entry}"
|
||||||
|
if [ -f "$log_path" ]; then
|
||||||
|
age_min=$(( ($(date +%s) - $(stat -c %Y "$log_path" 2>/dev/null || echo 0)) / 60 ))
|
||||||
|
last_active=$(date -u -d "@$(stat -c %Y "$log_path" 2>/dev/null || echo 0)" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")
|
||||||
|
agents=$(printf '%s' "$agents" | jq --arg n "$agent_name" --arg t "$last_active" --argjson a "$age_min" \
|
||||||
|
'. + [{ name: $n, last_active: $t, idle_minutes: $a }]')
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
jq -nc --argjson sessions "$active_sessions" --argjson agents "$agents" \
|
||||||
|
'{ active_sessions: $sessions, agents: $agents }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Assemble ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "Starting metrics collection"
|
||||||
|
|
||||||
|
PROJECTS_JSON="[]"
|
||||||
|
for toml in "$FACTORY_ROOT"/projects/*.toml; do
|
||||||
|
[ -f "$toml" ] || continue
|
||||||
|
log "Collecting metrics for $(basename "$toml")"
|
||||||
|
project_json=$(collect_project_metrics "$toml" 2>/dev/null || echo '{}')
|
||||||
|
PROJECTS_JSON=$(printf '%s\n%s' "$PROJECTS_JSON" "$project_json" | jq -s '.[0] + [.[1]]')
|
||||||
|
done
|
||||||
|
|
||||||
|
VAULT_JSON=$(collect_vault_metrics 2>/dev/null || echo '{}')
|
||||||
|
CI_JSON=$(collect_ci_metrics 2>/dev/null || echo '{}')
|
||||||
|
AGENTS_JSON=$(collect_agent_metrics 2>/dev/null || echo '{}')
|
||||||
|
|
||||||
|
# Build final output
|
||||||
|
jq -nc \
|
||||||
|
--arg ts "$NOW_ISO" \
|
||||||
|
--argjson projects "$PROJECTS_JSON" \
|
||||||
|
--argjson vault "$VAULT_JSON" \
|
||||||
|
--argjson ci "$CI_JSON" \
|
||||||
|
--argjson agents "$AGENTS_JSON" \
|
||||||
|
'{
|
||||||
|
generated_at: $ts,
|
||||||
|
projects: $projects,
|
||||||
|
vault: $vault,
|
||||||
|
ci: $ci,
|
||||||
|
agents: $agents
|
||||||
|
}' > "${OUTPUT_DIR}/metrics.json"
|
||||||
|
|
||||||
|
log "Metrics written to ${OUTPUT_DIR}/metrics.json"
|
||||||
527
site/dashboard.html
Normal file
527
site/dashboard.html
Normal file
|
|
@ -0,0 +1,527 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Disinto — Factory Dashboard</title>
|
||||||
|
<meta name="description" content="Live metrics from the Disinto autonomous code factory. PRs merged, issues closed, CI pass rate, vault decisions.">
|
||||||
|
<link rel="icon" href="favicon.ico" sizes="32x32">
|
||||||
|
<link rel="icon" href="favicon-192.png" sizes="192x192" type="image/png">
|
||||||
|
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
||||||
|
<link rel="canonical" href="https://disinto.ai/dashboard">
|
||||||
|
<meta property="og:title" content="Disinto — Factory Dashboard">
|
||||||
|
<meta property="og:description" content="Live metrics from the autonomous code factory.">
|
||||||
|
<meta property="og:url" content="https://disinto.ai/dashboard">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a0a;
|
||||||
|
--fg: #e0e0e0;
|
||||||
|
--dim: #707070;
|
||||||
|
--accent: #c8a46e;
|
||||||
|
--accent-dim: #8a7044;
|
||||||
|
--surface: #141414;
|
||||||
|
--border: #222;
|
||||||
|
--green: #4a7;
|
||||||
|
--red: #a54;
|
||||||
|
--yellow: #a93;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
line-height: 1.7;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .subtitle {
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .back {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
color: var(--accent-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .back:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
/* Timestamp */
|
||||||
|
.updated {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--dim);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.updated .dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--green);
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section headers */
|
||||||
|
.section-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent-dim);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat grid */
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat .value {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat .label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Project cards */
|
||||||
|
.projects {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project {
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 1.2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .name a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .name a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.project .metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .metric {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .metric .val {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .metric .lbl {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vault section */
|
||||||
|
.vault-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-item {
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-item .val {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-item .val.approved { color: var(--green); }
|
||||||
|
.vault-item .val.escalated { color: var(--yellow); }
|
||||||
|
.vault-item .val.rejected { color: var(--red); }
|
||||||
|
.vault-item .val.pending { color: var(--dim); }
|
||||||
|
|
||||||
|
.vault-item .lbl {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CI bar */
|
||||||
|
.ci-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-bar-container {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 1.2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-rate {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-rate .val {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-rate .lbl {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: var(--red);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-bar .fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--green);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-detail {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Agents */
|
||||||
|
.agents {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-list {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-row:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.agent-row .agent-name {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-row .agent-status {
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-row .agent-status.active {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading / error states */
|
||||||
|
.loading, .error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error { color: var(--red); }
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: var(--accent-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.header h1 { font-size: 1.5rem; }
|
||||||
|
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.vault-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.project .metrics { grid-template-columns: 1fr; gap: 0.5rem; }
|
||||||
|
.container { padding: 2rem 1rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1>Factory Dashboard</h1>
|
||||||
|
<div class="subtitle">live metrics from the disinto code factory</div>
|
||||||
|
<a class="back" href="/">← disinto.ai</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="updated" id="updated">
|
||||||
|
<span class="dot"></span>
|
||||||
|
Loading metrics...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<div class="loading">Fetching factory data...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div>Data refreshed every 6 hours by the metrics collector.</div>
|
||||||
|
<div style="margin-top:0.5rem">
|
||||||
|
<a href="https://codeberg.org/johba/disinto">source</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
var updated = document.getElementById('updated');
|
||||||
|
|
||||||
|
function el(tag, cls, html) {
|
||||||
|
var e = document.createElement(tag);
|
||||||
|
if (cls) e.className = cls;
|
||||||
|
if (html !== undefined) e.innerHTML = html;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeAgo(iso) {
|
||||||
|
var diff = (Date.now() - new Date(iso).getTime()) / 1000;
|
||||||
|
if (diff < 60) return 'just now';
|
||||||
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||||
|
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||||
|
return Math.floor(diff / 86400) + 'd ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
content.innerHTML = '';
|
||||||
|
|
||||||
|
// Updated timestamp
|
||||||
|
updated.innerHTML = '<span class="dot"></span> Updated ' + timeAgo(data.generated_at);
|
||||||
|
|
||||||
|
// Aggregate stats across projects
|
||||||
|
var totalPRsWeek = 0, totalPRsMonth = 0;
|
||||||
|
var totalIssuesWeek = 0, totalIssuesMonth = 0;
|
||||||
|
(data.projects || []).forEach(function (p) {
|
||||||
|
totalPRsWeek += (p.prs_merged || {}).week || 0;
|
||||||
|
totalPRsMonth += (p.prs_merged || {}).month || 0;
|
||||||
|
totalIssuesWeek += (p.issues_closed || {}).week || 0;
|
||||||
|
totalIssuesMonth += (p.issues_closed || {}).month || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Top stats
|
||||||
|
var statsDiv = el('div', 'stats');
|
||||||
|
var topMetrics = [
|
||||||
|
{ value: totalPRsWeek, label: 'PRs merged (week)' },
|
||||||
|
{ value: totalPRsMonth, label: 'PRs merged (month)' },
|
||||||
|
{ value: totalIssuesWeek, label: 'Issues closed (week)' },
|
||||||
|
{ value: totalIssuesMonth, label: 'Issues closed (month)' }
|
||||||
|
];
|
||||||
|
topMetrics.forEach(function (m) {
|
||||||
|
var stat = el('div', 'stat');
|
||||||
|
stat.appendChild(el('div', 'value', String(m.value)));
|
||||||
|
stat.appendChild(el('div', 'label', m.label));
|
||||||
|
statsDiv.appendChild(stat);
|
||||||
|
});
|
||||||
|
content.appendChild(statsDiv);
|
||||||
|
|
||||||
|
// Projects
|
||||||
|
if (data.projects && data.projects.length > 0) {
|
||||||
|
content.appendChild(el('div', 'section-title', 'Projects'));
|
||||||
|
var projectsDiv = el('div', 'projects');
|
||||||
|
data.projects.forEach(function (p) {
|
||||||
|
var card = el('div', 'project');
|
||||||
|
var nameDiv = el('div', 'name');
|
||||||
|
var nameLink = document.createElement('a');
|
||||||
|
nameLink.href = 'https://codeberg.org/' + p.repo;
|
||||||
|
nameLink.textContent = p.name;
|
||||||
|
nameDiv.appendChild(nameLink);
|
||||||
|
card.appendChild(nameDiv);
|
||||||
|
|
||||||
|
var metricsDiv = el('div', 'metrics');
|
||||||
|
var pm = [
|
||||||
|
{ val: ((p.prs_merged || {}).total || 0), lbl: 'PRs merged' },
|
||||||
|
{ val: ((p.issues_closed || {}).total || 0), lbl: 'Issues closed' },
|
||||||
|
{ val: ((p.backlog || {}).queued || 0), lbl: 'Backlog' }
|
||||||
|
];
|
||||||
|
pm.forEach(function (m) {
|
||||||
|
var metric = el('div', 'metric');
|
||||||
|
metric.appendChild(el('div', 'val', String(m.val)));
|
||||||
|
metric.appendChild(el('div', 'lbl', m.lbl));
|
||||||
|
metricsDiv.appendChild(metric);
|
||||||
|
});
|
||||||
|
card.appendChild(metricsDiv);
|
||||||
|
projectsDiv.appendChild(card);
|
||||||
|
});
|
||||||
|
content.appendChild(projectsDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vault
|
||||||
|
if (data.vault) {
|
||||||
|
content.appendChild(el('div', 'section-title', 'Vault Decisions'));
|
||||||
|
var vaultDiv = el('div', 'vault-grid');
|
||||||
|
var vaultItems = [
|
||||||
|
{ val: data.vault.approved || 0, cls: 'approved', lbl: 'Auto-approved' },
|
||||||
|
{ val: data.vault.escalated || 0, cls: 'escalated', lbl: 'Escalated' },
|
||||||
|
{ val: data.vault.rejected || 0, cls: 'rejected', lbl: 'Rejected' },
|
||||||
|
{ val: data.vault.pending || 0, cls: 'pending', lbl: 'Pending' }
|
||||||
|
];
|
||||||
|
vaultItems.forEach(function (v) {
|
||||||
|
var item = el('div', 'vault-item');
|
||||||
|
item.appendChild(el('div', 'val ' + v.cls, String(v.val)));
|
||||||
|
item.appendChild(el('div', 'lbl', v.lbl));
|
||||||
|
vaultDiv.appendChild(item);
|
||||||
|
});
|
||||||
|
content.appendChild(vaultDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CI
|
||||||
|
if (data.ci) {
|
||||||
|
content.appendChild(el('div', 'section-title', 'CI Pipeline (30 days)'));
|
||||||
|
var ciSection = el('div', 'ci-section');
|
||||||
|
var ciBar = el('div', 'ci-bar-container');
|
||||||
|
|
||||||
|
var rate = data.ci.pass_rate || 0;
|
||||||
|
var rateDiv = el('div', 'ci-rate');
|
||||||
|
var rateColor = rate >= 80 ? 'var(--green)' : rate >= 50 ? 'var(--yellow)' : 'var(--red)';
|
||||||
|
rateDiv.innerHTML = '<span class="val" style="color:' + rateColor + '">' + rate + '%</span>' +
|
||||||
|
'<span class="lbl">pass rate</span>';
|
||||||
|
ciBar.appendChild(rateDiv);
|
||||||
|
|
||||||
|
var bar = el('div', 'ci-bar');
|
||||||
|
bar.innerHTML = '<div class="fill" style="width:' + rate + '%"></div>';
|
||||||
|
ciBar.appendChild(bar);
|
||||||
|
|
||||||
|
ciBar.appendChild(el('div', 'ci-detail',
|
||||||
|
(data.ci.passed || 0) + ' passed / ' +
|
||||||
|
(data.ci.failed || 0) + ' failed / ' +
|
||||||
|
(data.ci.total || 0) + ' total'));
|
||||||
|
|
||||||
|
ciSection.appendChild(ciBar);
|
||||||
|
content.appendChild(ciSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agents
|
||||||
|
if (data.agents && data.agents.agents && data.agents.agents.length > 0) {
|
||||||
|
content.appendChild(el('div', 'section-title', 'Agent Activity'));
|
||||||
|
var agentsDiv = el('div', 'agents');
|
||||||
|
var agentList = el('div', 'agent-list');
|
||||||
|
|
||||||
|
data.agents.agents.forEach(function (a) {
|
||||||
|
var row = el('div', 'agent-row');
|
||||||
|
row.appendChild(el('span', 'agent-name', a.name));
|
||||||
|
var statusCls = a.idle_minutes < 60 ? 'agent-status active' : 'agent-status';
|
||||||
|
var statusText = a.idle_minutes < 60 ? 'active (' + a.idle_minutes + 'm)' : timeAgo(a.last_active);
|
||||||
|
row.appendChild(el('span', statusCls, statusText));
|
||||||
|
agentList.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
agentsDiv.appendChild(agentList);
|
||||||
|
content.appendChild(agentsDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('data/metrics.json')
|
||||||
|
.then(function (r) {
|
||||||
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(render)
|
||||||
|
.catch(function (err) {
|
||||||
|
content.innerHTML = '<div class="error">Could not load metrics.<br>' +
|
||||||
|
'<span style="font-size:0.7rem;color:var(--dim)">The collector runs every 6 hours. ' +
|
||||||
|
'Data may not be available yet.</span></div>';
|
||||||
|
updated.innerHTML = '<span class="dot" style="background:var(--red)"></span> Offline';
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -529,6 +529,7 @@
|
||||||
<div class="cta-links">
|
<div class="cta-links">
|
||||||
<a href="https://codeberg.org/johba/disinto">Browse the source</a>
|
<a href="https://codeberg.org/johba/disinto">Browse the source</a>
|
||||||
<a href="https://codeberg.org/johba/disinto/issues">Watch it work</a>
|
<a href="https://codeberg.org/johba/disinto/issues">Watch it work</a>
|
||||||
|
<a href="/dashboard">Live dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -536,6 +537,7 @@
|
||||||
<div>Built from scrap, powered by a single battery.</div>
|
<div>Built from scrap, powered by a single battery.</div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://codeberg.org/johba/disinto">codeberg.org/johba/disinto</a>
|
<a href="https://codeberg.org/johba/disinto">codeberg.org/johba/disinto</a>
|
||||||
|
<a href="/dashboard">dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="under-hood">
|
<div class="under-hood">
|
||||||
Under the hood: dev, review, planner, gardener, supervisor, vault — six agents orchestrated by cron and bash.
|
Under the hood: dev, review, planner, gardener, supervisor, vault — six agents orchestrated by cron and bash.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue