Skip to content

Commit 68eba48

Browse files
authored
new thingo's for admin dash (#242)
1 parent 2d296de commit 68eba48

6 files changed

Lines changed: 107 additions & 43 deletions

File tree

app/controllers/admin/metrics_controller.rb

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def index
121121
}
122122
}
123123

124+
review_stats = review_metrics(start_date.beginning_of_day..today.end_of_day)
125+
124126
render inertia: "Admin/Metrics/Index", props: {
125127
range_days: days,
126128
summary: {
@@ -158,7 +160,56 @@ def index
158160
},
159161
referral_economy: referral_economy,
160162
reel_economy: reel_economy,
161-
location_distribution: location_distribution
163+
location_distribution: location_distribution,
164+
reviews: review_stats
165+
}
166+
end
167+
168+
private
169+
170+
# Review throughput for completed reviews in the given window:
171+
# - avg_active_seconds: heartbeat-measured hands-on review time per review
172+
# - avg_wall_seconds: open → decision wall-clock per review
173+
# - avg_turnaround_seconds: builder submit → reviewer decision (queue + review)
174+
def review_metrics(window)
175+
completed = ReviewSession.completed.where(ended_at: window)
176+
completed_count = completed.count
177+
avg_active = completed.average(:active_seconds).to_f.round
178+
179+
wall_total = 0
180+
completed.where.not(started_at: nil).pluck(:started_at, :ended_at).each do |started_at, ended_at|
181+
wall_total += (ended_at - started_at).to_i
182+
end
183+
avg_wall = completed_count.positive? ? (wall_total / completed_count) : 0
184+
185+
decision_actions = %w[project.approved project.returned project.rejected project.pitch_approved]
186+
decisions = AuditEvent
187+
.where(action: decision_actions, target_type: "Project", created_at: window)
188+
.pluck(:target_id, :created_at)
189+
190+
subs_by_project = AuditEvent
191+
.where(action: "project.submitted_for_review", target_type: "Project", target_id: decisions.map(&:first).uniq)
192+
.pluck(:target_id, :created_at)
193+
.group_by(&:first)
194+
.transform_values { |rows| rows.map(&:last) }
195+
196+
turnaround_total = 0
197+
turnaround_count = 0
198+
decisions.each do |project_id, decided_at|
199+
submitted_at = subs_by_project[project_id]&.select { |t| t <= decided_at }&.max
200+
next unless submitted_at
201+
202+
turnaround_total += (decided_at - submitted_at).to_i
203+
turnaround_count += 1
204+
end
205+
avg_turnaround = turnaround_count.positive? ? (turnaround_total / turnaround_count) : 0
206+
207+
{
208+
completed: completed_count,
209+
avg_active_seconds: avg_active,
210+
avg_wall_seconds: avg_wall,
211+
avg_turnaround_seconds: avg_turnaround,
212+
turnaround_sample: turnaround_count
162213
}
163214
end
164215
end

app/controllers/admin/projects_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def index
2626
def pitches
2727
scope = policy_scope(Project).includes(:user, :ships).kept.where(status: :pitch_pending)
2828
scope = scope.search(params[:query]) if params[:query].present?
29-
@pagy, @projects = pagy(scope.order(created_at: :desc))
29+
@pagy, @projects = pagy(scope.order(created_at: :asc))
3030

3131
render inertia: "Admin/Projects/Index", props: {
3232
projects: @projects.map { |p| serialize_project_row(p) },

app/controllers/admin/review_audits_controller.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ class Admin::ReviewAuditsController < Admin::ApplicationController
33
before_action :set_session, only: [ :show ]
44

55
def index
6-
scope = ReviewSession.includes(:reviewer, :project).order(started_at: :desc)
6+
scope = ReviewSession.completed.includes(:reviewer, :project).order(ended_at: :desc)
77
scope = scope.where(reviewer_id: params[:reviewer_id]) if params[:reviewer_id].present?
88
scope = scope.where(project_id: params[:project_id]) if params[:project_id].present?
9-
scope = scope.where(decision: nil) if params[:status] == "open"
10-
scope = scope.where.not(decision: nil) if params[:status] == "closed"
119

1210
@pagy, @sessions = pagy(scope)
1311

@@ -16,8 +14,7 @@ def index
1614
pagy: pagy_props(@pagy),
1715
filters: {
1816
reviewer_id: params[:reviewer_id].to_s,
19-
project_id: params[:project_id].to_s,
20-
status: params[:status].to_s
17+
project_id: params[:project_id].to_s
2118
},
2219
totals: aggregate_totals
2320
}
@@ -53,7 +50,7 @@ def window_for(session)
5350
end
5451

5552
def aggregate_totals
56-
base = ReviewSession.all
53+
base = ReviewSession.completed
5754
by_active = base.group(:reviewer_id).sum(:active_seconds)
5855
counts = base.group(:reviewer_id).count
5956

@@ -72,7 +69,7 @@ def aggregate_totals
7269
reviewer_name: names[id] || "Unknown",
7370
active_seconds: by_active[id] || 0,
7471
wall_seconds: wall_by_id[id] || 0,
75-
sessions_count: counts[id] || 0
72+
reviews_count: counts[id] || 0
7673
}
7774
end
7875
end

app/javascript/pages/Admin/Metrics/Index.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ interface ReelEconomy {
7272
total_coins: number
7373
}
7474

75+
interface ReviewStats {
76+
completed: number
77+
avg_active_seconds: number
78+
avg_wall_seconds: number
79+
avg_turnaround_seconds: number
80+
turnaround_sample: number
81+
}
82+
7583
interface CountryRow {
7684
country: string
7785
count: number
@@ -118,6 +126,17 @@ function CountryBars({ rows, total }: { rows: CountryRow[]; total: number }) {
118126
)
119127
}
120128

129+
function formatDuration(s: number): string {
130+
if (!s || s <= 0) return '—'
131+
const d = Math.floor(s / 86400)
132+
const h = Math.floor((s % 86400) / 3600)
133+
const m = Math.floor((s % 3600) / 60)
134+
if (d > 0) return `${d}d ${h}h`
135+
if (h > 0) return `${h}h ${m}m`
136+
if (m > 0) return `${m}m ${s % 60}s`
137+
return `${s}s`
138+
}
139+
121140
function Stat({ label, value, hint, accent }: { label: string; value: string | number; hint?: string; accent?: boolean }) {
122141
return (
123142
<Card>
@@ -143,6 +162,7 @@ export default function AdminMetricsIndex({
143162
referral_economy,
144163
reel_economy,
145164
location_distribution,
165+
reviews,
146166
}: {
147167
range_days: number
148168
summary: Summary
@@ -156,6 +176,7 @@ export default function AdminMetricsIndex({
156176
referral_economy: ReferralEconomy
157177
reel_economy: ReelEconomy
158178
location_distribution: LocationDistribution
179+
reviews: ReviewStats
159180
}) {
160181
const max = Math.max(1, ...daily.map((d) => d.count))
161182
const maxHours = Math.max(1, ...daily_hours.map((d) => d.hours))
@@ -285,6 +306,20 @@ export default function AdminMetricsIndex({
285306
</div>
286307
</div>
287308

309+
<div className="space-y-2">
310+
<h2 className="text-sm font-semibold tracking-wide uppercase text-muted-foreground">Reviews — last {range_days} days</h2>
311+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
312+
<Stat label="Reviews completed" value={reviews.completed} hint="decisions made in range" accent />
313+
<Stat label="Avg review time" value={formatDuration(reviews.avg_active_seconds)} hint="active (heartbeat) time per review" />
314+
<Stat label="Avg open → decision" value={formatDuration(reviews.avg_wall_seconds)} hint="reviewer opened until decided" />
315+
<Stat
316+
label="Avg submit → decision"
317+
value={formatDuration(reviews.avg_turnaround_seconds)}
318+
hint={`builder submit until decided · n=${reviews.turnaround_sample}`}
319+
/>
320+
</div>
321+
</div>
322+
288323
<Card>
289324
<CardHeader>
290325
<CardTitle>Coin economy by tier</CardTitle>

app/javascript/pages/Admin/ReviewAudits/Index.tsx

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,9 @@ interface ReviewerTotal {
2525
reviewer_name: string
2626
active_seconds: number
2727
wall_seconds: number
28-
sessions_count: number
28+
reviews_count: number
2929
}
3030

31-
const filters = [
32-
{ key: '', label: 'All' },
33-
{ key: 'open', label: 'Open (in-progress)' },
34-
{ key: 'closed', label: 'Closed' },
35-
]
36-
3731
function decisionBadge(decision: string | null) {
3832
if (!decision) return <Badge variant="warning">in progress</Badge>
3933
switch (decision) {
@@ -76,7 +70,7 @@ function Leaderboard({
7670
<TableHead>Reviewer</TableHead>
7771
<TableHead>Active</TableHead>
7872
<TableHead>Wall</TableHead>
79-
<TableHead className="text-right">Sessions</TableHead>
73+
<TableHead className="text-right">Reviews</TableHead>
8074
</TableRow>
8175
</TableHeader>
8276
<TableBody>
@@ -102,7 +96,7 @@ function Leaderboard({
10296
<TableCell className={cn('font-mono text-sm', sortKey === 'wall_seconds' && 'font-semibold')}>
10397
{formatSeconds(t.wall_seconds)}
10498
</TableCell>
105-
<TableCell className="text-right font-mono text-xs">{t.sessions_count}</TableCell>
99+
<TableCell className="text-right font-mono text-xs">{t.reviews_count}</TableCell>
106100
</TableRow>
107101
))}
108102
</TableBody>
@@ -120,7 +114,7 @@ export default function ReviewAuditsIndex({
120114
}: {
121115
sessions: SessionRow[]
122116
pagy: PagyProps
123-
filters: { reviewer_id: string; project_id: string; status: string }
117+
filters: { reviewer_id: string; project_id: string }
124118
totals: ReviewerTotal[]
125119
}) {
126120
function applyFilter(key: string, value: string) {
@@ -135,65 +129,51 @@ export default function ReviewAuditsIndex({
135129
Review Audits
136130
</h1>
137131
<p className="text-sm text-muted-foreground mt-1">
138-
Every review session ever opened — reviewer, project, active seconds, and a full audit trail of what happened. Superadmin only.
132+
Every completed review — reviewer, project, active seconds, and a full audit trail of what happened. Superadmin only.
139133
</p>
140134
</div>
141135

142136
{totals.length === 0 ? (
143137
<Card>
144-
<CardContent className="p-6 text-sm text-muted-foreground text-center">No closed sessions yet.</CardContent>
138+
<CardContent className="p-6 text-sm text-muted-foreground text-center">No completed reviews yet.</CardContent>
145139
</Card>
146140
) : (
147141
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
148142
<Leaderboard
149143
title="Active time leaderboard"
150-
description="Sum of heartbeat-measured active time across all sessions. The basis for reviewer payouts."
144+
description="Sum of heartbeat-measured active time across completed reviews. The basis for reviewer payouts."
151145
totals={totals}
152146
sortKey="active_seconds"
153147
onPickReviewer={(id) => applyFilter('reviewer_id', String(id))}
154148
/>
155149
<Leaderboard
156150
title="Wall-clock leaderboard"
157-
description="Sum of started_at→ended_at durations across all sessions. Includes idle/AFK time. Use the gap vs active to spot inefficient reviewers."
151+
description="Sum of started_at→ended_at durations across completed reviews. Includes idle/AFK time. Use the gap vs active to spot inefficient reviewers."
158152
totals={totals}
159153
sortKey="wall_seconds"
160154
onPickReviewer={(id) => applyFilter('reviewer_id', String(id))}
161155
/>
162156
</div>
163157
)}
164158

165-
<div className="flex gap-2 flex-wrap items-center">
166-
{filters.map((opt) => (
167-
<button
168-
key={opt.key}
169-
onClick={() => applyFilter('status', opt.key)}
170-
className={cn(
171-
'px-3 py-1.5 text-xs font-medium rounded-md border transition-colors cursor-pointer',
172-
f.status === opt.key
173-
? 'bg-primary text-primary-foreground border-primary'
174-
: 'border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground',
175-
)}
176-
>
177-
{opt.label}
178-
</button>
179-
))}
180-
{(f.reviewer_id || f.project_id) && (
159+
{(f.reviewer_id || f.project_id) && (
160+
<div className="flex gap-2 flex-wrap items-center">
181161
<button
182162
onClick={() => router.get('/admin/review_audits')}
183163
className="text-xs text-muted-foreground hover:text-foreground cursor-pointer underline"
184164
>
185165
clear filters
186166
</button>
187-
)}
188-
</div>
167+
</div>
168+
)}
189169

190170
<Card>
191171
<CardHeader>
192-
<CardTitle>Sessions</CardTitle>
172+
<CardTitle>Completed reviews</CardTitle>
193173
</CardHeader>
194174
<CardContent className="space-y-4">
195175
{sessions.length === 0 ? (
196-
<p className="text-sm text-muted-foreground py-10 text-center">No sessions match.</p>
176+
<p className="text-sm text-muted-foreground py-10 text-center">No completed reviews match.</p>
197177
) : (
198178
<Table>
199179
<TableHeader>

app/models/review_session.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class ReviewSession < ApplicationRecord
3232

3333
scope :active, -> { where(ended_at: nil) }
3434
scope :ended, -> { where.not(ended_at: nil) }
35+
scope :completed, -> { where.not(decision: nil) }
3536
scope :for_reviewer, ->(user) { where(reviewer_id: user.id) }
3637
scope :for_project, ->(project) { where(project_id: project.id) }
3738

0 commit comments

Comments
 (0)