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
11 changes: 11 additions & 0 deletions Trsr.Api/Controllers/HealthController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;

namespace Trsr.Api.Controllers;

[ApiController]
[Route("api/health")]
public class HealthController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(new { status = "ok" });
}
5 changes: 5 additions & 0 deletions frontend/src/app/app.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host {
display: block;
width: 100%;
height: 100%;
}
23 changes: 23 additions & 0 deletions frontend/src/app/core/api/health.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

const POLL_INTERVAL_MS = 10_000;

@Injectable({ providedIn: 'root' })
export class HealthService {
private readonly http = inject(HttpClient);

readonly isOnline = signal<boolean>(false);

constructor() {
this.check();
setInterval(() => this.check(), POLL_INTERVAL_MS);
}

private check() {
this.http.get('/api/health').subscribe({
next: () => this.isOnline.set(true),
error: () => this.isOnline.set(false),
});
}
}
10 changes: 10 additions & 0 deletions frontend/src/app/core/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ export interface ModelBreakdownDto {
avgDurationMs: number;
}

export interface LatencyStatDto {
endpointId: string;
p50Ms: number;
p95Ms: number;
p99Ms: number;
minMs: number;
maxMs: number;
sampleCount: number;
}

export interface AgentCallFilter {
projectId?: string;
agentId?: string;
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/app/core/api/statistics.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { SummaryDto, ModelBreakdownDto } from './models';
import { SummaryDto, ModelBreakdownDto, LatencyStatDto } from './models';

@Injectable({ providedIn: 'root' })
export class StatisticsService {
Expand All @@ -13,9 +13,17 @@ export class StatisticsService {
return this.http.get<SummaryDto>('/api/statistics/summary', { params });
}

getModelBreakdown(from?: string): Observable<ModelBreakdownDto[]> {
getLatency(filter: { from?: string; agentId?: string } = {}): Observable<LatencyStatDto[]> {
let params = new HttpParams();
if (from) params = params.set('from', from);
if (filter.from) params = params.set('from', filter.from);
if (filter.agentId) params = params.set('agentId', filter.agentId);
return this.http.get<LatencyStatDto[]>('/api/statistics/latency', { params });
}

getModelBreakdown(filter: { from?: string; agentId?: string } = {}): Observable<ModelBreakdownDto[]> {
let params = new HttpParams();
if (filter.from) params = params.set('from', filter.from);
if (filter.agentId) params = params.set('agentId', filter.agentId);
return this.http.get<ModelBreakdownDto[]>('/api/statistics/model-breakdown', { params });
}
}
4 changes: 1 addition & 3 deletions frontend/src/app/core/shell/shell.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<!-- App Shell: floating Apple-style sidebar layout -->
<div style="display:flex; height:100vh; overflow:hidden; background:var(--bg-primary); position:relative; z-index:1;">

<!-- Floating Sidebar -->
<aside
Expand Down Expand Up @@ -237,8 +236,7 @@
</header>

<!-- Page content -->
<main style="flex:1; overflow-y:auto; padding:16px 10px; background:transparent; position:relative; z-index:0;">
<main style="flex:1; overflow:hidden; padding:16px 10px; background:transparent; position:relative; z-index:0; display:flex; flex-direction:column;">
<router-outlet />
</main>
</div>
</div>
3 changes: 3 additions & 0 deletions frontend/src/app/core/shell/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ interface NavItem {
styles: `
:host {
display: flex;
width: 100%;
height: 100vh;
overflow: hidden;
background-color: var(--bg-primary);
position: relative;
z-index: 1;
}
`,
})
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/features/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Output ONLY the JSON. No prose. No markdown.`,
@Component({
selector: 'app-agents',
templateUrl: './agents.html',
styles: ``,
styles: `:host { display: block; flex: 1; min-height: 0; overflow-y: auto; }`,
})
export class Agents {
readonly agentColors = AGENT_COLORS;
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/app/features/dashboard/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
<h1 style="font-size:24px; font-weight:700; letter-spacing:-0.02em; margin:0;">Dashboard</h1>
<span style="
display:flex; align-items:center; gap:5px;
font-size:11px; color:var(--success); font-weight:600;
padding:3px 8px; background:var(--success-subtle);
border:1px solid rgba(16,185,129,0.2); border-radius:100px;
font-size:11px; font-weight:600;
padding:3px 8px; border-radius:100px;
white-space:nowrap; flex-shrink:0;
">
<span style="width:6px; height:6px; border-radius:50%; background:var(--success);" class="pulse-dot"></span>
Proxy online
" [style.color]="health.isOnline() ? 'var(--success)' : 'var(--danger)'"
[style.background]="health.isOnline() ? 'var(--success-subtle)' : 'var(--danger-subtle)'"
[style.border]="health.isOnline() ? '1px solid rgba(16,185,129,0.2)' : '1px solid rgba(239,68,68,0.2)'">
<span style="width:6px; height:6px; border-radius:50%;"
[class.pulse-dot]="health.isOnline()"
[style.background]="health.isOnline() ? 'var(--success)' : 'var(--danger)'"></span>
{{ health.isOnline() ? 'Proxy online' : 'Proxy offline' }}
</span>
</div>
<p style="font-size:13.5px; color:var(--text-muted); margin:0;">Overview of your agent tracing and evaluation activity.</p>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/app/features/dashboard/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, OnInit, inject, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { StatisticsService } from '../../core/api/statistics.service';
import { AgentCallsService } from '../../core/api/agent-calls.service';
import { HealthService } from '../../core/api/health.service';
import { SummaryDto, AgentCallDto } from '../../core/api/models';

type LoadState = 'loading' | 'loaded' | 'error';
Expand Down Expand Up @@ -34,12 +35,13 @@ interface AgentCard {
selector: 'app-dashboard',
imports: [RouterLink],
templateUrl: './dashboard.html',
styles: ``,
styles: `:host { display: block; flex: 1; min-height: 0; overflow-y: auto; }`,
})
export class Dashboard implements OnInit {
readonly Math = Math;
private readonly statisticsService = inject(StatisticsService);
private readonly agentCallsService = inject(AgentCallsService);
readonly health = inject(HealthService);

readonly summaryState = signal<LoadState>('loading');
readonly summary = signal<SummaryDto | null>(null);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/features/providers/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const PROVIDER_COLORS: Record<string, string> = {
selector: 'app-providers',
imports: [FormsModule],
templateUrl: './providers.html',
styles: `:host { display: block; flex: 1; min-height: 0; overflow-y: auto; }`,
})
export class Providers implements OnInit {
private readonly svc = inject(ProvidersService);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/features/runs/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const RUNS_DATA: Run[] = [
@Component({
selector: 'app-runs',
templateUrl: './runs.html',
styles: ``,
styles: `:host { display: block; flex: 1; min-height: 0; overflow-y: auto; }`,
})
export class Runs {
readonly Math = Math;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/features/suites/suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const SUITES_DATA: Suite[] = [
@Component({
selector: 'app-suites',
templateUrl: './suites.html',
styles: ``,
styles: `:host { display: block; flex: 1; min-height: 0; overflow-y: auto; }`,
})
export class Suites {
readonly Math = Math;
Expand Down
85 changes: 70 additions & 15 deletions frontend/src/app/features/traces/traces.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<div style="max-width:1320px; margin:0 auto; display:flex; flex-direction:column; gap:14px; padding-bottom:24px;">
<div style="width:100%; max-width:1320px; margin:0 auto; display:flex; flex-direction:column; gap:14px; flex:1; min-height:0; padding-bottom:16px;">

<!-- Header -->
<div class="fade-up" style="display:flex; align-items:flex-start; justify-content:space-between; gap:16px;">
<div>
<div style="display:flex; align-items:center; gap:10px; margin-bottom:6px;">
<h1 style="font-size:24px; font-weight:700; letter-spacing:-0.02em; margin:0;">Traces</h1>
<span style="display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--success); font-weight:600; padding:3px 8px; background:var(--success-subtle); border-radius:100px; white-space:nowrap;">
<span style="width:6px; height:6px; border-radius:50%; background:var(--success);" class="pulse-dot"></span>
Live
<span style="display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:600; padding:3px 8px; border-radius:100px; white-space:nowrap;"
[style.color]="health.isOnline() ? 'var(--success)' : 'var(--danger)'"
[style.background]="health.isOnline() ? 'var(--success-subtle)' : 'var(--danger-subtle)'"
[style.border]="health.isOnline() ? '1px solid rgba(16,185,129,0.2)' : '1px solid rgba(239,68,68,0.2)'">
<span style="width:6px; height:6px; border-radius:50%;"
[class.pulse-dot]="health.isOnline()"
[style.background]="health.isOnline() ? 'var(--success)' : 'var(--danger)'"></span>
{{ health.isOnline() ? 'Live' : 'Offline' }}
</span>
</div>
<p style="font-size:13.5px; color:var(--text-muted); margin:0;">Every LLM call captured by the proxy, grouped by model.</p>
Expand Down Expand Up @@ -46,13 +51,40 @@ <h1 style="font-size:24px; font-weight:700; letter-spacing:-0.02em; margin:0;">T
<div style="background:var(--bg-card); border-radius:14px; padding:14px; box-shadow:var(--shadow-card); display:flex; flex-direction:column;">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:6px;">
<span style="font-size:12px; font-weight:600;">Latency</span>
<span style="font-size:10.5px; color:var(--text-muted);" class="mono">p95 · 4.1s</span>
<span style="font-size:10.5px; color:var(--text-muted);" class="mono">p95 · {{ p95Label() }}</span>
</div>
<svg viewBox="0 0 280 56" width="100%" height="56" style="display:block;" preserveAspectRatio="none">
@for (b of histBars; track b.x) {
<rect [attr.x]="b.x.toFixed(1)" [attr.y]="b.y.toFixed(1)" [attr.width]="b.w.toFixed(1)" [attr.height]="b.h.toFixed(1)" fill="#06b6d4" opacity="0.8" rx="2"/>
<!-- Wrapper provides relative positioning for the tooltip -->
<div style="position:relative;">
@if (hoveredBar(); as bar) {
<div style="
position:absolute; bottom:calc(100% + 6px); z-index:20;
transform:translateX(-50%);
background:var(--bg-card-2); border:1px solid var(--hairline);
border-radius:8px; padding:6px 10px;
font-size:11px; white-space:nowrap; pointer-events:none;
box-shadow:var(--shadow-float);
" [style.left]="((bar.x + bar.w / 2) / 280 * 100) + '%'">
<div style="font-weight:600; color:var(--text-primary); font-family:'JetBrains Mono',monospace;">{{ bar.label }}</div>
<div style="color:var(--text-muted); margin-top:3px;">
~{{ bar.pct.toFixed(0) }}% of requests
@if (bar.count > 0) { · {{ bar.count }} calls }
</div>
</div>
}
</svg>
<svg viewBox="0 0 280 56" width="100%" height="56" style="display:block; cursor:crosshair;" preserveAspectRatio="none">
@for (b of histBars(); track b.x) {
<rect
[attr.x]="b.x.toFixed(1)" [attr.y]="b.y.toFixed(1)"
[attr.width]="b.w.toFixed(1)" [attr.height]="b.h.toFixed(1)"
rx="2"
[attr.fill]="hoveredBar() === b ? '#22d3ee' : '#06b6d4'"
[attr.opacity]="hoveredBar() !== null && hoveredBar() !== b ? '0.4' : '0.85'"
(mouseenter)="hoveredBar.set(b)"
(mouseleave)="hoveredBar.set(null)"
/>
}
</svg>
</div>
</div>
</div>

Expand Down Expand Up @@ -121,10 +153,33 @@ <h1 style="font-size:24px; font-weight:700; letter-spacing:-0.02em; margin:0;">T
</div>
}
</div>
<button style="display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:8px; font-size:12px; font-weight:500; background:var(--bg-card-2); color:var(--text-primary); box-shadow:var(--shadow-pill);">
<span style="color:var(--text-muted);">Range:</span><span>Last 24h</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<!-- Range filter -->
<div style="position:relative;">
<button (click)="toggleRangeDropdown()"
style="display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:8px; font-size:12px; font-weight:500; background:var(--bg-card-2); color:var(--text-primary); box-shadow:var(--shadow-pill);">
<span style="color:var(--text-muted);">Range:</span>
<span>{{ rangeLabelFor(rangeKey()) }}</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
@if (rangeDropdownOpen()) {
<div (click)="closeRangeDropdown()" style="position:fixed; inset:0; z-index:10;"></div>
<div style="position:absolute; top:calc(100% + 6px); left:0; min-width:160px;
background:var(--bg-card-2); border:1px solid var(--hairline); border-radius:10px;
box-shadow:var(--shadow-float); z-index:11; overflow:hidden;">
@for (r of ranges; track r.key) {
<button (click)="setRange(r.key)"
style="width:100%; text-align:left; padding:9px 12px; font-size:12px; display:block; transition:background 0.1s;"
[style.background]="rangeKey() === r.key ? 'var(--accent-subtle)' : 'transparent'"
[style.color]="rangeKey() === r.key ? 'var(--accent-hover)' : 'var(--text-secondary)'"
[style.border-top]="$index > 0 ? '1px solid var(--hairline)' : 'none'"
onmouseenter="if(!this.style.background.includes('accent'))this.style.background='rgba(255,255,255,0.04)'"
onmouseleave="if(!this.style.background.includes('accent'))this.style.background='transparent'">
{{ r.label }}
</button>
}
</div>
}
</div>
</div>

<!-- Results count -->
Expand All @@ -137,9 +192,9 @@ <h1 style="font-size:24px; font-weight:700; letter-spacing:-0.02em; margin:0;">T
}

<!-- Table -->
<div class="fade-up" style="animation-delay:160ms; background:var(--bg-card); border-radius:16px; box-shadow:var(--shadow-card); overflow:hidden;">
<div class="fade-up" style="animation-delay:160ms; background:var(--bg-card); border-radius:16px; box-shadow:var(--shadow-card); overflow:hidden; flex:1; min-height:0; overflow-y:auto; display:flex; flex-direction:column;">
<!-- Header -->
<div style="display:grid; grid-template-columns:1.6fr 1.5fr 0.7fr 1.1fr 1.1fr 0.9fr; padding:12px 18px; font-size:10.5px; font-weight:600; color:var(--text-muted); letter-spacing:0.06em; text-transform:uppercase; border-bottom:1px solid var(--hairline);">
<div style="display:grid; grid-template-columns:1.6fr 1.5fr 0.7fr 1.1fr 1.1fr 0.9fr; padding:12px 18px; font-size:10.5px; font-weight:600; color:var(--text-muted); letter-spacing:0.06em; text-transform:uppercase; border-bottom:1px solid var(--hairline); position:sticky; top:0; background:var(--bg-card); z-index:1; flex-shrink:0;">
<span>Trace ID</span>
<span>Model</span>
<span>Status</span>
Expand Down
Loading
Loading