Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"type": "module",
"dependencies": {
"@astrojs/starlight": "^0.40.0",
"astro": "^6.4.6",
"mermaid": "^11.6.0",
"sharp": "^0.34.0",
"unist-util-visit": "^5.0.0"
"astro": "^6.4.8",
"mermaid": "^11.15.0",
"sharp": "^0.35.2",
"unist-util-visit": "^5.1.0"
}
}
16 changes: 8 additions & 8 deletions apps/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
"preview": "vite preview"
},
"devDependencies": {
"@solidjs/router": "^0.15.0",
"@unocss/preset-icons": "^0.65.0",
"@unocss/preset-uno": "^0.65.0",
"@unocss/transformer-variant-group": "^0.65.0",
"solid-js": "^1.9.0",
"unocss": "^0.65.0",
"vite": "^6.0.0",
"vite-plugin-solid": "^2.11.0"
"@solidjs/router": "^0.16.1",
"@unocss/preset-icons": "^66.7.2",
"@unocss/preset-uno": "^66.7.2",
"@unocss/transformer-variant-group": "^66.7.2",
"solid-js": "^1.9.13",
"unocss": "^66.7.2",
"vite": "^8.0.16",
"vite-plugin-solid": "^2.11.12"
}
}
127 changes: 127 additions & 0 deletions apps/insights/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export interface RunSummaryRow {
endedAt: number
cacheHit: boolean | null
hash: string
cpuMs: number | null
peakRssBytes: number | null
wallclockStartNs: string | null
wallclockEndNs: string | null
}
Expand Down Expand Up @@ -97,12 +99,73 @@ export interface CacheStats {

export interface TaskHistoryRow {
id: string
project: string
task: string
runs: number
successes: number
failures: number
hits: number
successRate: number
hitRate: number
failureMode: 'stable' | 'flaky-recoverable' | 'flaky-fatal'
p50DurationMs: number | undefined
p99DurationMs: number | undefined
minDurationMs: number | undefined
maxDurationMs: number | undefined
avgDurationMs: number | undefined
totalDurationMs: number
lastSeenAt: number | undefined
}

export interface TopTaskRow {
id: string
project: string
task: string
runs: number
totalDurationMs: number
avgDurationMs: number
}

export interface FailureRow {
runId: string | null
project: string
task: string
exitCode: number
durationMs: number
startedAt: number
hash: string
}

export interface CacheEntryRow {
hash: string
project: string
task: string
command: string
exitCode: number
durationMs: number
sizeBytes: number
createdAt: number
accessedAt: number
}

export interface CacheProjectRow {
project: string
entries: number
totalBytes: number
}

export interface CacheSavings {
hitsLast24h: number
estimatedTimeSavedMs: number
estimatedTimeSavedTotalMs: number
}

export interface TaskDetail {
project: string
task: string
aggregate: TaskHistoryRow | null
recent: RunSummaryRow[]
latestEntry: CacheEntryRow | null
}

export interface CacheKeyExplanation {
Expand Down Expand Up @@ -174,3 +237,67 @@ export async function getHistory(args: { limit?: number } = {}): Promise<TaskHis
export async function explainCacheKey(taskId: string): Promise<CacheKeyExplanation> {
return await getJson<CacheKeyExplanation>(`/v1/explain/${encodeURIComponent(taskId)}`)
}

export async function getTopTasks(limit = 10): Promise<TopTaskRow[]> {
const r = await getJson<{ tasks: TopTaskRow[] }>(`/v1/top-tasks?limit=${limit}`)
return r.tasks
}

export async function getFailures(limit = 25): Promise<FailureRow[]> {
const r = await getJson<{ failures: FailureRow[] }>(`/v1/failures?limit=${limit}`)
return r.failures
}

export async function getCacheSavings(): Promise<CacheSavings> {
return await getJson<CacheSavings>('/v1/cache/savings')
}

export async function getCacheBreakdown(limit = 20): Promise<CacheProjectRow[]> {
const r = await getJson<{ projects: CacheProjectRow[] }>(`/v1/cache/breakdown?limit=${limit}`)
return r.projects
}

export async function listCacheEntries(
args: {
limit?: number
orderBy?: 'created_at' | 'accessed_at' | 'size_bytes' | 'duration_ms'
project?: string
} = {},
): Promise<CacheEntryRow[]> {
const params = new URLSearchParams()
if (args.limit !== undefined) params.set('limit', String(args.limit))
if (args.orderBy !== undefined) params.set('orderBy', args.orderBy)
if (args.project !== undefined) params.set('project', args.project)
const r = await getJson<{ entries: CacheEntryRow[] }>(`/v1/cache/entries?${params.toString()}`)
return r.entries
}

export async function getTaskDetail(taskId: string): Promise<TaskDetail | null> {
try {
return await getJson<TaskDetail>(`/v1/tasks/${encodeURIComponent(taskId)}`)
} catch (err) {
if (err instanceof Error && err.message.includes('404')) return null
throw err
}
}

/**
* Subscribe to live event stream via SSE. Returns an unsubscribe fn.
* The hosted SPA uses this to overlay running tasks on the Overview.
*/
export function subscribeEvents(onMessage: (event: unknown) => void): () => void {
const origin = getOrigin()
const source = new EventSource(`${origin}/v1/events`)
source.onmessage = (e) => {
try {
onMessage(JSON.parse(e.data))
} catch {
// ignore malformed
}
}
source.onerror = () => {
// Connection lost — EventSource will auto-retry. The picker's
// status dot reflects connectedness via the /version probe.
}
return () => source.close()
}
14 changes: 14 additions & 0 deletions apps/insights/src/components/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ export const Shell: ParentComponent = (props) => {
>
Overview
</A>
<A
href="/tasks"
class="text-fg-muted hover:text-fg no-underline text-sm"
activeClass="text-fg"
>
Tasks
</A>
<A
href="/cache"
class="text-fg-muted hover:text-fg no-underline text-sm"
activeClass="text-fg"
>
Cache
</A>
</nav>
<Show
when={editing()}
Expand Down
67 changes: 67 additions & 0 deletions apps/insights/src/components/Sparkline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Tiny inline SVG sparkline — no chart library. Renders a line + an
// optional cache-hit dot marker series, sized to its container.

import { For } from 'solid-js'

export interface SparkPoint {
value: number
hit?: boolean
}

export function Sparkline(props: {
data: readonly SparkPoint[]
width?: number
height?: number
strokeClass?: string
}) {
const w = () => props.width ?? 280
const h = () => props.height ?? 36
const data = () => props.data
const max = () => Math.max(1, ...data().map((p) => p.value))
const min = () => Math.min(...data().map((p) => p.value), 0)
const range = () => Math.max(1, max() - min())

const xs = () => {
const n = data().length
if (n <= 1) return [w() / 2]
return data().map((_, i) => (i / (n - 1)) * w())
}
const ys = () => data().map((p) => h() - ((p.value - min()) / range()) * h())

const linePath = () => {
const X = xs()
const Y = ys()
if (X.length === 0) return ''
return X.map((x, i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${Y[i]!.toFixed(1)}`).join(' ')
}

const areaPath = () => {
const X = xs()
const Y = ys()
if (X.length === 0) return ''
const line = X.map((x, i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${Y[i]!.toFixed(1)}`).join(' ')
return `${line} L${X[X.length - 1]!.toFixed(1)},${h()} L${X[0]!.toFixed(1)},${h()} Z`
}

return (
<svg viewBox={`0 0 ${w()} ${h()}`} width={w()} height={h()} class="block">
<path d={areaPath()} class="fill-accent/10" />
<path
d={linePath()}
class={props.strokeClass ?? 'stroke-accent'}
fill="none"
stroke-width="1.5"
/>
<For each={data()}>
{(pt, i) => (
<circle
cx={xs()[i()]!.toFixed(1)}
cy={ys()[i()]!.toFixed(1)}
r={pt.hit ? 2 : 1.5}
class={pt.hit ? 'fill-cache' : 'fill-accent'}
/>
)}
</For>
</svg>
)
}
6 changes: 6 additions & 0 deletions apps/insights/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { HashRouter, Route } from '@solidjs/router'
import 'virtual:uno.css'
import { Shell } from './components/Shell.tsx'
import { Overview } from './pages/Overview.tsx'
import { Tasks } from './pages/Tasks.tsx'
import { TaskDetail } from './pages/TaskDetail.tsx'
import { CachePage } from './pages/CachePage.tsx'
import { RunDetail } from './pages/RunDetail.tsx'

const root = document.getElementById('root')
Expand All @@ -12,6 +15,9 @@ render(
() => (
<HashRouter root={Shell}>
<Route path="/" component={Overview} />
<Route path="/tasks" component={Tasks} />
<Route path="/tasks/:id" component={TaskDetail} />
<Route path="/cache" component={CachePage} />
<Route path="/runs/:id" component={RunDetail} />
</HashRouter>
),
Expand Down
Loading
Loading