Files
ghtracker/src/components/FleetOverview.vue
T
lukaszraczylo 5313b56774 feat: add GitHub repository dashboard with Prometheus metrics
ghtracker authenticates as a GitHub App and scans a configured list of repositories on a schedule. It serves a dashboard, a JSON state endpoint, a health probe and Prometheus metrics.

The dashboard lists only items that need attention: failing workflows, pull requests that wait too long or fail their checks, stale issues and projects that failed to scan. It has search, dynamic filters and a button that rescans on demand. The Vue app is embedded in the Go binary.

The client protects the GitHub rate limit with conditional requests, a request reserve and Retry-After handling.
2026-10-02 04:28:52 +01:00

123 lines
4.0 KiB
Vue

<script setup lang="ts">
import {
faCodePullRequest,
faCircleDot,
faGears,
faTriangleExclamation,
type IconDefinition,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed } from 'vue'
import FleetStrip from '@/components/FleetStrip.vue'
import type { Repo } from '@/lib/types'
import type { Facts, RepoCounts } from '@/stores/tracker'
const props = defineProps<{
crit: number
warn: number
blockCount: number
repos: Repo[]
counts: Map<string, RepoCounts>
facts: Facts
focused: string | null
matching: Set<string>
}>()
defineEmits<{ select: [fullName: string] }>()
const rows = computed<{ label: string; value: number; icon: IconDefinition; tone: string }[]>(
() => [
{
label: 'Workflows failing',
value: props.facts.failingWorkflows,
icon: faGears,
tone: 'text-crit',
},
{
label: 'Pull requests waiting',
value: props.facts.waitingPrs,
icon: faCodePullRequest,
tone: 'text-crit',
},
{
label: 'PRs with failing checks',
value: props.facts.failingChecks,
icon: faCodePullRequest,
tone: 'text-warn',
},
{ label: 'Stale issues', value: props.facts.staleIssues, icon: faCircleDot, tone: 'text-warn' },
{
label: 'Data problems',
value: props.facts.dataProblems,
icon: faTriangleExclamation,
tone: 'text-warn',
},
],
)
</script>
<template>
<section
class="grid grid-cols-[minmax(0,1fr)] items-end gap-x-14 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]"
aria-label="Fleet status"
>
<div>
<p class="text-muted-foreground mb-3 text-sm" data-test="scope">
<template v-if="blockCount > 0"
>{{ blockCount }} of {{ repos.length }} projects need attention</template
>
<template v-else>All {{ repos.length }} projects are healthy</template>
</p>
<h1
class="flex flex-wrap items-baseline gap-x-10 gap-y-1 font-semibold tracking-tight"
data-test="headline"
>
<template v-if="crit > 0 || warn > 0">
<span v-if="crit > 0" class="text-crit">
<span class="text-7xl leading-none tabular-nums">{{ crit }}</span>
<span class="ml-2 text-xl font-medium">critical</span>
</span>
<span v-if="warn > 0" class="text-warn">
<span class="text-7xl leading-none tabular-nums">{{ warn }}</span>
<span class="ml-2 text-xl font-medium">{{ warn === 1 ? 'warning' : 'warnings' }}</span>
</span>
</template>
<span v-else class="text-ok text-7xl leading-none">All clear</span>
</h1>
<div class="mt-8">
<FleetStrip
:repos="repos"
:counts="counts"
:focused="focused"
:matching="matching"
@select="$emit('select', $event)"
/>
<p class="text-muted-foreground mt-3 flex flex-wrap items-center gap-x-5 gap-y-1 text-xs">
<span class="flex items-center gap-1.5"
><span class="bg-crit size-2 rounded-[2px]"></span>critical</span
>
<span class="flex items-center gap-1.5"
><span class="bg-warn size-2 rounded-[2px]"></span>warning</span
>
<span class="flex items-center gap-1.5"
><span class="bg-ok/40 size-2 rounded-[2px]"></span>healthy</span
>
<span class="ml-auto">Select a segment to focus a project</span>
</p>
</div>
</div>
<dl class="border-border divide-border divide-y border-y text-sm" data-test="facts">
<div v-for="row in rows" :key="row.label" class="flex items-center gap-3 py-2.5">
<FontAwesomeIcon :icon="row.icon" class="text-muted-foreground w-4" aria-hidden="true" />
<dt class="flex-1">{{ row.label }}</dt>
<dd
class="font-mono text-base tabular-nums"
:class="row.value > 0 ? row.tone : 'text-muted-foreground'"
>
{{ row.value }}
</dd>
</div>
</dl>
</section>
</template>