Skip to content

Commit 5501f2d

Browse files
Dbochmanclaude
andauthored
feat: add SLO uptime calculator project (#89)
Interactive calculator that helps engineers understand achievable SLAs based on incident response times. Two modes: "What SLA can I achieve?" and "Can I meet this SLA target?" Features: - Response time sliders for each incident phase (alert latency, acknowledge, travel, authenticate, diagnose, fix) - Toggle visibility to exclude phases from calculations - Preset profiles (Incident Commander, On-Call Working Hours, After Hours) - Direct SLA percentage input with slider sync - Budget breakdown visualization showing time allocation per phase - Response overhead insight showing pre-diagnosis overhead - Monthly and yearly downtime calculations Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3ce47ba commit 5501f2d

11 files changed

Lines changed: 1202 additions & 10 deletions

File tree

package-lock.json

Lines changed: 101 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
"@radix-ui/react-dialog": "^1.1.2",
4040
"@radix-ui/react-dropdown-menu": "^2.1.16",
4141
"@radix-ui/react-label": "^2.1.8",
42+
"@radix-ui/react-progress": "^1.1.8",
4243
"@radix-ui/react-select": "^2.2.6",
44+
"@radix-ui/react-slider": "^1.3.6",
4345
"@radix-ui/react-slot": "^1.2.4",
4446
"@radix-ui/react-switch": "^1.2.6",
4547
"@radix-ui/react-tabs": "^1.1.13",
@@ -114,4 +116,3 @@
114116
"wait-on": "^9.0.3"
115117
}
116118
}
117-

public/sitemap.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
<changefreq>weekly</changefreq>
1313
<priority>0.9</priority>
1414
</url>
15+
<url>
16+
<loc>https://dylanbochman.com/projects</loc>
17+
<lastmod>2026-01-12</lastmod>
18+
<changefreq>weekly</changefreq>
19+
<priority>0.8</priority>
20+
</url>
1521
<url>
1622
<loc>https://dylanbochman.com/blog/2026-01-11-shaving-minutes-off-deploys</loc>
1723
<lastmod>2026-01-11</lastmod>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Slider } from '@/components/ui/slider';
2+
import { Label } from '@/components/ui/label';
3+
import { Card, CardContent } from '@/components/ui/card';
4+
5+
interface IncidentInputProps {
6+
value: number;
7+
onChange: (value: number) => void;
8+
}
9+
10+
export function IncidentInput({ value, onChange }: IncidentInputProps) {
11+
return (
12+
<Card>
13+
<CardContent className="pt-6">
14+
<div className="space-y-2">
15+
<div className="flex justify-between">
16+
<div>
17+
<Label htmlFor="incidents" className="text-sm font-medium">
18+
Expected incidents per month
19+
</Label>
20+
<p className="text-xs text-muted-foreground">
21+
How many incidents typically require response?
22+
</p>
23+
</div>
24+
<span className="text-sm font-mono tabular-nums text-primary">
25+
{value} {value === 1 ? 'incident' : 'incidents'}
26+
</span>
27+
</div>
28+
<Slider
29+
id="incidents"
30+
value={[value]}
31+
onValueChange={([v]) => onChange(v)}
32+
min={0}
33+
max={20}
34+
step={1}
35+
className="w-full"
36+
/>
37+
</div>
38+
</CardContent>
39+
</Card>
40+
);
41+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Slider } from '@/components/ui/slider';
2+
import { Label } from '@/components/ui/label';
3+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
4+
import { Eye, EyeOff } from 'lucide-react';
5+
import { Button } from '@/components/ui/button';
6+
import type { ResponseProfile } from './calculations';
7+
import type { EnabledPhases } from './index';
8+
9+
interface ResponseTimeInputsProps {
10+
profile: ResponseProfile;
11+
enabledPhases: EnabledPhases;
12+
onChange: (field: keyof ResponseProfile, value: number) => void;
13+
onToggle: (field: keyof ResponseProfile) => void;
14+
}
15+
16+
interface InputConfig {
17+
field: keyof ResponseProfile;
18+
label: string;
19+
description: string;
20+
max: number;
21+
}
22+
23+
const INPUT_CONFIG: InputConfig[] = [
24+
{
25+
field: 'alertLatencyMin',
26+
label: 'Alert latency',
27+
description: 'Condition true → alert fires',
28+
max: 30,
29+
},
30+
{
31+
field: 'acknowledgeMin',
32+
label: 'Time to acknowledge',
33+
description: 'Alert fires → you see it',
34+
max: 30,
35+
},
36+
{
37+
field: 'travelMin',
38+
label: 'Time to computer',
39+
description: 'Get to your workstation',
40+
max: 60,
41+
},
42+
{
43+
field: 'authMin',
44+
label: 'Time to authenticate',
45+
description: 'VPN, SSO, internal tools',
46+
max: 20,
47+
},
48+
{
49+
field: 'diagnoseMin',
50+
label: 'Time to diagnose',
51+
description: 'Investigate underlying cause(s)',
52+
max: 60,
53+
},
54+
{
55+
field: 'fixMin',
56+
label: 'Time to fix',
57+
description: 'Implement the fix',
58+
max: 120,
59+
},
60+
];
61+
62+
export function ResponseTimeInputs({
63+
profile,
64+
enabledPhases,
65+
onChange,
66+
onToggle,
67+
}: ResponseTimeInputsProps) {
68+
return (
69+
<Card>
70+
<CardHeader className="pb-4">
71+
<CardTitle className="text-base">Response Times (per incident)</CardTitle>
72+
</CardHeader>
73+
<CardContent className="space-y-6">
74+
{INPUT_CONFIG.map(({ field, label, description, max }) => {
75+
const isEnabled = enabledPhases[field];
76+
return (
77+
<div key={field} className={`space-y-2 ${!isEnabled ? 'opacity-50' : ''}`}>
78+
<div className="flex justify-between items-start">
79+
<div className="flex items-start gap-2">
80+
<Button
81+
variant="ghost"
82+
size="icon"
83+
className="h-6 w-6 shrink-0"
84+
onClick={() => onToggle(field)}
85+
aria-label={isEnabled ? `Exclude ${label} from calculation` : `Include ${label} in calculation`}
86+
>
87+
{isEnabled ? (
88+
<Eye className="h-4 w-4" />
89+
) : (
90+
<EyeOff className="h-4 w-4" />
91+
)}
92+
</Button>
93+
<div>
94+
<Label htmlFor={field} className="text-sm font-medium">
95+
{label}
96+
</Label>
97+
<p className="text-xs text-muted-foreground">{description}</p>
98+
</div>
99+
</div>
100+
<span className="text-sm font-mono tabular-nums text-primary">
101+
{isEnabled ? `${profile[field]} min` : 'excluded'}
102+
</span>
103+
</div>
104+
<Slider
105+
id={field}
106+
value={[profile[field]]}
107+
onValueChange={([value]) => onChange(field, value)}
108+
min={0}
109+
max={max}
110+
step={1}
111+
className="w-full"
112+
disabled={!isEnabled}
113+
/>
114+
</div>
115+
);
116+
})}
117+
</CardContent>
118+
</Card>
119+
);
120+
}

0 commit comments

Comments
 (0)