Skip to content

Commit d803689

Browse files
committed
feat: Integrated uptime results into status page
1 parent e523e40 commit d803689

File tree

3 files changed

+160
-15
lines changed

3 files changed

+160
-15
lines changed

src/app/pages/advanced/status.page.html

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,32 @@ <h1 class="mb-4">Service Statuses</h1>
2323
<!-- Current Status -->
2424
<div class="p-card px-4 py-3">
2525
<h2 class="mb-4">Current Service Status</h2>
26-
<!--
27-
<div *ngIf="statusInfo$ | async">
26+
27+
<div *ngIf="internalStatusInfo$ | async as internal">
2828
<p-divider class="my-4" />
29-
<h3>Domain Locker Internal</h3>
29+
<h3>Web Application</h3>
3030
<ul class="list-none p-0 mt-2">
31-
<li *ngFor="let s of dlServicesToSetup" class="flex">
32-
<span class="font-medium flex-shrink-0 w-56 flex items-center space-x-2">{{s}}</span>
33-
<span class="flex items-center"><i class="pi pi-check-circle text-green-400 mr-1"></i> Operational</span>
31+
<li *ngFor="let s of currentStatuses" class="flex">
32+
<details>
33+
<summary class="flex cursor-pointer hover:bg-surface-100 rounded pr-2">
34+
<span class="font-medium flex-shrink-0 w-56 flex items-center space-x-2">{{s.name}}</span>
35+
<span *ngIf="s.status; else notOperational" class="flex items-center"><i class="pi pi-check-circle text-green-400 mr-1"></i> Operational</span>
36+
<ng-template #notOperational>
37+
<span class="flex items-center"><i class="pi pi-times-circle text-red-400 mr-1"></i> Unavailable</span>
38+
</ng-template>
39+
</summary>
40+
<p *ngIf="s.extraInfo" class="mt-0 text-sm opacity-70 italic">
41+
Historical uptime is {{ s.extraInfo.uptimePercent }}% with an average ping of {{ s.extraInfo.averagePing }}ms
42+
(latency range is {{s.extraInfo.minPing}} to {{s.extraInfo.maxPing}}ms).
43+
</p>
44+
</details>
3445
</li>
3546
</ul>
36-
</div> -->
37-
47+
</div>
48+
49+
3850
<div *ngIf="internalStatusInfo$ | async as internal">
39-
<!-- <p-divider class="my-4" /> -->
51+
<p-divider class="my-4" />
4052
<h3>Database</h3>
4153
<ul class="list-none p-0 mt-2">
4254
<li class="flex">
@@ -125,6 +137,12 @@ <h3>Third-Party Services</h3>
125137
</div>
126138
</div>
127139

140+
<!-- App response and uptime chart -->
141+
<div *ngIf="uptimeChartUrl" class="p-card px-4 py-3">
142+
<h2 class="mt-4">Recent Response Time History</h2>
143+
<img [src]="uptimeChartUrl" alt="Uptime response time chart" class="w-full max-w-[48rem] mx-auto flex" />
144+
</div>
145+
128146
<!-- Recent Incidents -->
129147
<div class="p-card px-4 py-3" *ngIf="statusInfo$ | async as statusInfo; else loading" >
130148
<h2 class="mb-4">Third-Party Disruptions</h2>

src/app/pages/advanced/status.page.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ export interface InternalStatus {
3939
database: any;
4040
}
4141

42+
interface HeartBeat {
43+
status: number,
44+
time: string,
45+
msg?: string,
46+
ping: number,
47+
}
48+
49+
interface StatusMetrics {
50+
uptimePercent: number;
51+
averagePing: number;
52+
minPing: number;
53+
maxPing: number;
54+
}
55+
56+
4257
@Component({
4358
standalone: true,
4459
imports: [CommonModule, PrimeNgModule, DomainFaviconComponent],
@@ -56,6 +71,17 @@ export default class StatusPage {
5671

5772
public dlServicesToSetup: string[] = ['App', 'API', 'Database', 'Auth', 'Scheduler'];
5873

74+
readonly uptimeServiceMap: Record<string, string> = {
75+
'5': 'Landing',
76+
'6': 'Main App',
77+
'7': 'Docs',
78+
};
79+
80+
// public uptimeExtraInfo
81+
82+
public uptimeChartUrl: string = '';
83+
public currentStatuses: { id: string; name: string; status: boolean; latestPing?: number, extraInfo?: any }[] = [];
84+
5985
constructor(
6086
private http: HttpClient,
6187
private errorHandler: ErrorHandlerService,
@@ -67,13 +93,15 @@ export default class StatusPage {
6793
const servicePieConfig = this.generatePieChartConfig(data, 'service', { title: 'Issues per Service' });
6894
this.pieChartUrl = 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(servicePieConfig));
6995
});
96+
97+
this.internalStatusInfo$.subscribe(this.uptimeDataProcess.bind(this));
7098
}
7199

72100
private fetchInternalStatusData(): Observable<any> {
73101
return this.http.get<any>('/api/internal-status-info').pipe(
74102
map(data => ({
75103
scheduled: data.scheduledCrons || [],
76-
supabase: data.supabaseStatus?.healthy || { healthy: false, undetermined: true },
104+
supabase: data.supabaseStatus || { healthy: false, undetermined: true },
77105
uptime: data.uptimeStatus || {},
78106
database: data.databaseStatus || {},
79107
})),
@@ -126,6 +154,22 @@ export default class StatusPage {
126154
return /^#([0-9A-F]{3}){1,2}$/i.test(value) ? value : fallback;
127155
}
128156

157+
private calculateStatusMetrics(heartbeats: HeartBeat[] | undefined): StatusMetrics | null {
158+
if (!heartbeats || heartbeats.length === 0) return null;
159+
160+
const successful = heartbeats.filter(h => h.status === 1);
161+
const pings = successful.map(h => h.ping).filter(p => typeof p === 'number');
162+
163+
if (pings.length === 0) return null;
164+
165+
const uptimePercent = Math.round((successful.length / heartbeats.length) * 100);
166+
const averagePing = Math.round(pings.reduce((a, b) => a + b, 0) / pings.length);
167+
const minPing = Math.min(...pings);
168+
const maxPing = Math.max(...pings);
169+
170+
return { uptimePercent, averagePing, minPing, maxPing };
171+
}
172+
129173
/**
130174
* Generates a Chart.js configuration object for a stacked bar chart.
131175
*
@@ -387,4 +431,87 @@ export default class StatusPage {
387431
return {};
388432
}
389433

434+
private uptimeDataProcess(internal: InternalStatus): void {
435+
const rawHeartbeats = internal.uptime?.heartbeatList || {};
436+
const statusData: typeof this.currentStatuses = [];
437+
438+
const chartColors = [
439+
'--purple-400', '--teal-400', '--pink-400', '--blue-400', '--yellow-400', '--green-400',
440+
];
441+
442+
const datasets = Object.entries(rawHeartbeats).map(([id, heartbeats], i) => {
443+
const serviceName = this.uptimeServiceMap[id] || `Service ${id}`;
444+
const latest = (heartbeats as HeartBeat[]).at(-1);
445+
446+
statusData.push({
447+
id,
448+
name: serviceName,
449+
status: latest?.status === 1,
450+
latestPing: latest?.ping,
451+
extraInfo: this.calculateStatusMetrics(heartbeats as HeartBeat[]),
452+
});
453+
454+
return {
455+
label: serviceName,
456+
data: (heartbeats as HeartBeat[]).map(h => ({
457+
x: h.time.replace(' ', 'T') + 'Z',
458+
y: h.ping
459+
})),
460+
borderColor: this.getCssVariableColor(chartColors[i % chartColors.length], '#36A2EB'),
461+
fill: false,
462+
spanGaps: false,
463+
tension: 0.4,
464+
};
465+
});
466+
467+
this.currentStatuses = statusData;
468+
469+
// Just using the first dataset's timestamps to generate HH:mm labels
470+
const firstDataset = datasets[0]?.data || [];
471+
const labels = firstDataset.map((point: any) => {
472+
const date = new Date(point.x);
473+
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
474+
});
475+
476+
const chartConfig = {
477+
type: 'line',
478+
data: {
479+
labels,
480+
datasets
481+
},
482+
options: {
483+
title: {
484+
display: true,
485+
text: 'Domain Locker Uptime',
486+
color: this.getCssVariableColor('--text-color', '#333'),
487+
},
488+
responsive: true,
489+
plugins: {
490+
title: {
491+
display: true,
492+
text: 'Service Response Time',
493+
color: this.getCssVariableColor('--text-color', '#333'),
494+
}
495+
},
496+
scales: {
497+
x: {
498+
title: {
499+
display: true,
500+
text: 'Time'
501+
}
502+
},
503+
y: {
504+
title: {
505+
display: true,
506+
text: 'Ping (ms)'
507+
}
508+
}
509+
}
510+
}
511+
};
512+
513+
this.uptimeChartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify(chartConfig))}`;
514+
}
515+
516+
390517
}

src/server/routes/internal-status-info.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
1616
fetchSupabaseHealth(),
1717
fetchUptimeStatus(),
1818
]);
19-
19+
2020
return { scheduledCrons, databaseStatus, supabaseStatus, uptimeStatus };
2121
});
2222

@@ -53,13 +53,13 @@ async function fetchDatabaseHealth(authHeader?: string): Promise<any> {
5353

5454

5555
async function fetchSupabaseHealth(): Promise<{ healthy?: boolean } | undefined> {
56-
const apiKey = import.meta.env['SUPABASE_ANON_KEY'];
57-
if (!apiKey) return {};
56+
const anonKey = import.meta.env['SUPABASE_ANON_KEY'];
57+
if (!anonKey) return {};
5858
try {
5959
const res = await fetch(`${import.meta.env['SUPABASE_URL']}/health`, {
6060
headers: {
61-
apikey: apiKey,
62-
Authorization: `Bearer ${apiKey}`,
61+
apikey: anonKey,
62+
Authorization: `Bearer ${anonKey}`,
6363
},
6464
signal: AbortSignal.timeout(timeout),
6565
});

0 commit comments

Comments
 (0)