Uncomment out the healthcheck endpoints Change the calculation of healthy/unhealthy to count degraded as healthy Change the calculation of an entire service so that ANY unhealthy makes the whole service list as unhealthy
1072 lines
No EOL
50 KiB
HTML
1072 lines
No EOL
50 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>API Health Check Dashboard</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 1.8em;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.controls {
|
|
padding: 20px 30px;
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #e9ecef;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
}
|
|
|
|
.refresh-btn {
|
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4);
|
|
}
|
|
|
|
.refresh-btn:disabled {
|
|
background: #6c757d;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.last-updated {
|
|
color: #6c757d;
|
|
font-size: 14px;
|
|
}
|
|
|
|
|
|
.microservice-group {
|
|
margin: 0 30px;
|
|
border: 1px solid #e9ecef;
|
|
border-top: none;
|
|
overflow: hidden;
|
|
transition: box-shadow 0.3s ease;
|
|
}
|
|
|
|
.microservice-group:first-child {
|
|
margin-top: 20px;
|
|
border-top: 1px solid #e9ecef;
|
|
border-radius: 8px 8px 0 0;
|
|
}
|
|
|
|
.microservice-group:last-child {
|
|
margin-bottom: 20px;
|
|
border-radius: 0 0 8px 8px;
|
|
}
|
|
|
|
.microservice-group:first-child:last-child {
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.microservice-header {
|
|
background: #f8f9fa;
|
|
padding: 15px 20px;
|
|
border-bottom: 1px solid #e9ecef;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
|
|
.microservice-header:hover {
|
|
background: #e9ecef;
|
|
}
|
|
|
|
.microservice-name {
|
|
font-size: 1.3em;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.microservice-header-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.microservice-header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.endpoint-stats {
|
|
display: flex;
|
|
gap: 8px;
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
.stat-pill {
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.stat-pill-healthy {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.stat-pill-healthy.empty {
|
|
background: #e9ecef;
|
|
color: #6c757d;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.stat-pill-degraded {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.stat-pill-degraded.empty {
|
|
background: #e9ecef;
|
|
color: #6c757d;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.stat-pill-unhealthy {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.stat-pill-unhealthy.empty {
|
|
background: #e9ecef;
|
|
color: #6c757d;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.collapse-icon {
|
|
font-size: 1.2em;
|
|
transition: transform 0.3s ease;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.collapse-icon.expanded {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.microservice-content {
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease-out;
|
|
}
|
|
|
|
.microservice-content.expanded {
|
|
max-height: none;
|
|
transition: max-height 0.5s ease-in;
|
|
}
|
|
|
|
|
|
.endpoint {
|
|
border-bottom: 1px solid #f1f3f4;
|
|
padding: 20px;
|
|
}
|
|
|
|
.endpoint:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.endpoint-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
.endpoint-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex: 1;
|
|
}
|
|
|
|
.endpoint-header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.endpoint-refresh-btn {
|
|
background: #007bff;
|
|
color: white;
|
|
border: none;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.endpoint-refresh-btn:hover {
|
|
background: #0056b3;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.endpoint-refresh-btn:disabled {
|
|
background: #6c757d;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.endpoint-refresh-btn .spinner {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-width: 2px;
|
|
margin: 0;
|
|
}
|
|
|
|
.endpoint-url {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
background: #f8f9fa;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.endpoint-method {
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.method-get {
|
|
background: #d1ecf1;
|
|
color: #0c5460;
|
|
}
|
|
|
|
.method-post {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.method-put {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.method-delete {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.status-code {
|
|
font-weight: 600;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.status-2xx {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.status-3xx {
|
|
background: #cce5ff;
|
|
color: #004085;
|
|
}
|
|
|
|
.status-4xx {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.status-5xx {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.status-error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.response-time {
|
|
color: #6c757d;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.request-response {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.data-section {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.data-section h4 {
|
|
margin-bottom: 8px;
|
|
color: #495057;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.data-content {
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 4px;
|
|
padding: 12px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 3px solid #f3f3f3;
|
|
border-top: 3px solid #007bff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(255, 255, 255, 0.85);
|
|
display: none;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9999;
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
.loading-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.loading-overlay-content {
|
|
text-align: center;
|
|
background: white;
|
|
padding: 30px 50px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.loading-overlay .spinner {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-width: 5px;
|
|
margin: 0 auto 20px;
|
|
}
|
|
|
|
.loading-overlay-text {
|
|
font-size: 18px;
|
|
color: #2c3e50;
|
|
font-weight: 600;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.error-message {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
padding: 15px;
|
|
margin: 20px 30px;
|
|
border-radius: 6px;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
.controls {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.endpoint-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.microservice-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>API Health Check Dashboard</h1>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button id="refreshBtn" class="refresh-btn">
|
|
<span id="refreshText">Refresh All</span>
|
|
</button>
|
|
<div class="at-a-glance">
|
|
<span id="at-a-glance-text" class="empty stat-pill stat-pill-degraded">Loading...</span>
|
|
</div>
|
|
<div class="last-updated" id="lastUpdated">
|
|
Never updated
|
|
</div>
|
|
</div>
|
|
|
|
<div id="content">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
Loading health check data...
|
|
</div>
|
|
</div>
|
|
|
|
<div id="loadingOverlay" class="loading-overlay">
|
|
<div class="loading-overlay-content">
|
|
<div class="spinner"></div>
|
|
<div class="loading-overlay-text">Refreshing health checks...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const environmentContext = getCurrentEnvironmentData();
|
|
const apiUrl = "https://" + environmentContext.apiHostname;
|
|
const todaysDate = new Date();
|
|
const twoWeeksInTheFutureDate = new Date((new Date()).setDate((new Date()).getDate() + 14));
|
|
const startDate = todaysDate.getFullYear() + "-" + (todaysDate.getMonth() + 1) + "-" + todaysDate.getDate();
|
|
const endDate = twoWeeksInTheFutureDate.getFullYear() + "-" + (twoWeeksInTheFutureDate.getMonth() + 1) + "-" + twoWeeksInTheFutureDate.getDate();
|
|
|
|
const degradedResponseThreshold = 5000;
|
|
const unhealthyResponseThreshold = 30000;
|
|
|
|
// Configuration for microservices and their endpoints
|
|
const microservices = {
|
|
'AccountService': {
|
|
baseUrl: apiUrl + '/account/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
{ path: '/account/insurance-companies', method: 'GET' },
|
|
{ path: '/account/account-details/373341', method: 'GET' },
|
|
{ path: '/account/bill-to-account-number?ParentAccountNumber=373341&ProviderNumber=01820&IsItac=false&TypeOfClaim=Personal&LineOfBusiness=Commercial', method: 'GET' }
|
|
]
|
|
},
|
|
'LocationService': {
|
|
baseUrl: apiUrl + '/location/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
{ path: '/location/zip/43015/Replace', method: 'GET' },
|
|
{ path: '/location/alert-reasons/03357', method: 'GET' },
|
|
{ path: '/location/providers/43015/Replace/100/167132/true/CR00046193/false', method: 'GET' },
|
|
{ path: '/location/serviceability-details?zip=43015&carId=CR00046193&parentAccountNumber=167132&referralSequenceNumber='+environmentContext.referralNumber+'&applicationName=FixMyGlassNextGen&lineItems[0].partNumber=FW02018GBNN&glassPieces[0].location=Windshield&glassPieces[0].name=Single&isHeavyTruckVehicle=false', method: 'GET' }
|
|
]
|
|
},
|
|
'VehicleService': {
|
|
baseUrl: apiUrl + '/vehicle/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
{ path: '/vehicle/years', method: 'GET' },
|
|
{ path: '/vehicle/makes/1997', method: 'GET' },
|
|
{ path: '/vehicle/Models/1997/Toyota', method: 'GET' },
|
|
{ path: '/vehicle/Styles/1997/Toyota/Camry', method: 'GET' },
|
|
{ path: '/vehicle/lookup/1997/Toyota/Camry/4%20door%20sedan', method: 'GET' }
|
|
]
|
|
},
|
|
'PartService': {
|
|
baseUrl: apiUrl + '/parts/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
{ path: '/parts/damage-options/CR00046193', method: 'GET' },
|
|
{ path: '/parts/rain-repel', method: 'GET' },
|
|
{ path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} },
|
|
{ path: '/parts/wipers/CR00046193/43015', method: 'GET' },
|
|
{ path: '/parts/mobile-premium-fee/Cash/Replace', method: 'GET' },
|
|
{ path: '/parts/service-package-discount', method: 'POST', body: {"glassPieces":[{"location":"Windshield","name":"Single"}],"damageType":"Replace","AppName":"FixMyGlass"} },
|
|
{ path: '/parts/mobile-fee/?serviceType=Install&facilityType=Mobile&parentAccountNumber=167132&billToAccountNumber=87291&providerNumber=03357&isItacOptimized=false&zipCode=43015', method: 'GET' },
|
|
]
|
|
},
|
|
// The two requests that need serverData are commented out
|
|
'PriceService': {
|
|
baseUrl: apiUrl + '/price/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
{ path: '/price/taxed-order-items?ParentAccountNumber=167132&BillToAccountNumber=87291&ProviderNumber=003357&AppointmentType=Mobile&ServiceLocation.City=asdf&ServiceLocation.State=OH&ServiceLocation.ZipCode=43015&lineItems[0].partNumber=FW02018GBNN&lineItems[0].laborAmount=60&lineItems[0].kitPrice=0&lineItems[0].sellingPrice=297.33&lineItems[1].partNumber=GGG%202018&lineItems[1].laborAmount=0&lineItems[1].kitPrice=0&lineItems[1].sellingPrice=13.4&lineItems[2].partNumber=RECYCLE%20FEE&lineItems[2].laborAmount=39.99&lineItems[2].kitPrice=0&lineItems[2].sellingPrice=0&ServerData=' + encodeURIComponent(environmentContext.serverData), method: 'GET' },
|
|
{ path: '/price/order-items', method: 'POST', body: { "parentAccountNumber": 167132, "billToAccountNumber": "87291", "providerNumber": "03357", "carId": "CR00046193", "year": 1997, "make": "Toyota", "model": "Camry", "eon": environmentContext.eon, "state": "OH", "referralNumber": environmentContext.referralNumber, "referralSequenceNumber": environmentContext.referralNumber, "zipCode": "43015", "serviceZipCode": "43015", "referralDate": "2025-10-29T10:55:27.18", "isReplacement": true, "deductible": 0, "coverageStatus": null, "lineItems": [ { "partNumber": "RECYCLE FEE" }, { "partNumber": "FW02018GBNN" }, { "partNumber": "GGG 2018" }, { "partNumber": "SBB19" }, { "partNumber": "SBB21" } ], "serverData": "", "AppName": "FixMyGlass" } },
|
|
{ path: '/price/order-promo', method: 'POST', body: {"promoCode":"WEBSL45","addableVaps":[{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null}],"order":{"appointmentType":"Mobile","carId":"CR00046193","correlationId":"b0261e48-1111-44fc-abc9-34b41380687c","eon":environmentContext.eon,"isInsurance":false,"isRepair":false,"glassToReplace":[{"location":"Windshield","name":"Single"}],"lineItemsOnOrder":[{"partNumber":"FW02018GBNN","basePartNumber":"FW02018","description":"solar","color":"Green Tint, Blue Shade","partType":"WINDSHIELD","canSafeliteRecalibrate":false,"requiresRecalibration":false,"requiresCapabilityQuestions":false,"recalibrationType":null,"childParts":[{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94}],"kitPrice":0,"sellingPrice":368.94,"laborAmount":60,"id":"4326cee6-b39e-41ff-bb29-df9220e32298","isChildPart":false,"salesTax":30.02},{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94},{"partNumber":"RECYCLE FEE","description":null,"partType":"REPLACE FEE","isInsurable":null,"isChildPart":false,"laborAmount":39.99,"sellingPrice":0,"kitPrice":0,"salesTax":2.8,"id":"3771aed5-0de1-4f0e-b1ad-394f73e08f38","cartItemType":"REPLACE FEE"},{"partNumber":"MOBILE FEE","description":null,"partType":"MOBILE FEE","isInsurable":true,"isChildPart":false,"laborAmount":0,"sellingPrice":0,"kitPrice":0,"salesTax":0,"id":"2bc936ec-ef5e-4fe2-95fb-e946792b1ae5"},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":3.15,"cartItemType":"RAIN REPEL"}],"parentAccountNumber":"167132","referralSequenceNumber":environmentContext.referralNumber,"serviceState":"OH","serverData":environmentContext.serverData,"vehicleYear":"1997","zipCodeOrProviderCtu":"03357"},"AppName":"FixMyGlass"} },
|
|
{ path: '/price/revalidated-order-promos', method: 'POST', body: {"inactivePromos":[],"order":{"appointmentType":"Mobile","carId":"CR00046193","correlationId":"b0261e48-1111-44fc-abc9-34b41380687c","eon":environmentContext.eon,"isInsurance":false,"isRepair":false,"glassToReplace":[{"location":"Windshield","name":"Single"}],"lineItemsOnOrder":[{"partNumber":"FW02018GBNN","basePartNumber":"FW02018","description":"solar","color":"Green Tint, Blue Shade","partType":"WINDSHIELD","canSafeliteRecalibrate":false,"requiresRecalibration":false,"requiresCapabilityQuestions":false,"recalibrationType":null,"childParts":[{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94}],"kitPrice":0,"sellingPrice":368.94,"laborAmount":60,"id":"4326cee6-b39e-41ff-bb29-df9220e32298","isChildPart":false,"salesTax":30.02},{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94},{"partNumber":"RECYCLE FEE","description":null,"partType":"REPLACE FEE","isInsurable":null,"isChildPart":false,"laborAmount":39.99,"sellingPrice":0,"kitPrice":0,"salesTax":2.8,"id":"3771aed5-0de1-4f0e-b1ad-394f73e08f38","cartItemType":"REPLACE FEE"},{"partNumber":"MOBILE FEE","description":null,"partType":"MOBILE FEE","isInsurable":true,"isChildPart":false,"laborAmount":0,"sellingPrice":0,"kitPrice":0,"salesTax":0,"id":"2bc936ec-ef5e-4fe2-95fb-e946792b1ae5"},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"discountedLineItemIds":["4db19a8b-739f-4f7b-983e-f6308c5f5901","e27ae20a-3a60-4fdf-a738-4b56d2e761cb"],"partType":"PROMO_DISCOUNT","promoCode":"1WIPER0","partNumber":"WIPER DISCOUNT","laborAmount":0,"sellingPrice":-10,"kitPrice":0,"salesTax":-0.71,"isChildPart":false,"cartItemType":"1WIPER0"}],"parentAccountNumber":"167132","referralSequenceNumber":environmentContext.referralNumber,"serviceState":"OH","serverData":environmentContext.serverData,"vehicleYear":"1997","zipCodeOrProviderCtu":"03357"},"AppName":"FixMyGlass"} }
|
|
]
|
|
},
|
|
'ScheduleService': {
|
|
baseUrl: apiUrl + '/schedule/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
{ path: '/schedule/mobile-time-slots', method: 'POST', body: {"startDate":startDate,"endDate":endDate,"applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"SBB19","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"SBB21","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"RAIN REPEL","partType":"RAIN REPEL","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"zipCode":"43015","AppName":"FixMyGlass"} },
|
|
{ path: '/schedule/shop-time-slots', method: 'POST', body: {"providerNumber":"003417","startDate":startDate,"endDate":endDate,"shopAppointmentType":"InshopOrDropoff","applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"AppName":"FixMyGlass"} }
|
|
]
|
|
},
|
|
'AnalyticsService': {
|
|
baseUrl: apiUrl + '/analytics/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
]
|
|
},
|
|
'ExperimentService': {
|
|
baseUrl: apiUrl + '/experiments/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
]
|
|
},
|
|
'OrderService': {
|
|
baseUrl: apiUrl + '/order/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
]
|
|
},
|
|
'ContentService': {
|
|
baseUrl: apiUrl + '/content/api/v1',
|
|
endpoints: [
|
|
{ path: '/healthcheck', method: 'GET' },
|
|
{ path: '/content/fmg/vehicle', method: 'GET' },
|
|
]
|
|
}
|
|
};
|
|
|
|
let healthData = {};
|
|
let isRefreshing = false;
|
|
|
|
// Initialize the dashboard
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.getElementById('refreshBtn').addEventListener('click', refreshAllEndpoints);
|
|
refreshAllEndpoints();
|
|
});
|
|
|
|
// This is hardcoded to prod now. The tax endpoint will need a payload per environment as the actual prices on the items could change and a mismatch causes it to fail.
|
|
// The promo endpoints should have promos that work in each of their respective environments as well
|
|
function getCurrentEnvironmentData() {
|
|
const environmentData = [
|
|
{
|
|
name: 'unhosted',
|
|
hostName: '',
|
|
apiHostname: 'digitalapi.safelite.io',
|
|
serverData: 'IUvCHjQRpYbtCcBWWWLiZFwAeebAcoHY4ScYoV79A4PLvf9LiLL3LqsoeYRpegxMdvVhyDnXcDLLmkLE3kRQDqiRGDW2wvnXgL+fjzYL0p95qxanec6WFfogz3OaK83adokw/erCFXgtUDuEPEMS5rdde5jFb70ZVMp3u0tM63qB+UfBVDiBDMnbrhwD+/hMnQovOITTZekN95ZTjWMT/JYxg0WEBbsm26WB59l16sGk5AHwCx4+cRco1l6Hg6zQhEeJLZRTB8Ic8knOhAvrec19+QH9un1yB0RiV1KcbjJO1U7NM54OjY8/42xEMqM1omoHnR88CQNT1Fex3oTFj+2ub8qmGicMEGOAOfuMGiHkRmObiyAGo80rbeUroJEWndL7FG1xXI1j+Rv5Zrxp4ATosTIulGq1G5og+bORbetx6fXbWxqB2lEcAGpOSx+BriVApKo+EvrFdlTWRbG01MJJwy95iidJHxy32l65UQpfnZNBUfaT02OVwcinhhoT9K5vJ+AUD6kJXFeGTIiM87H9IbcmNJXjgPJSAzK7nBX3bogx4ch9P3VMnw477kBDIuTKS9+H3pLUkgCEb/3rykwXh/lLF0YjJ7AQ4MdeCTruIXWntYkBcXrG9r10WFhera8KnKdj3/DoSLZAAXIc+JQXI9n0Nihtr3hGl8bn1bw4IMWTr7TlfemLkEbXMnIiJc4+H8aucXVJo/nDcDMexA==',
|
|
referralNumber: '39747322',
|
|
eon: 'S39734693',
|
|
debug: true,
|
|
},
|
|
{
|
|
name: 'Localhost',
|
|
hostName: 'localhost',
|
|
apiHostname: 'digitalapi.dev.safelite.io',
|
|
serverData: 'IUvCHjQRpYbtCcBWWWLiZFwAeebAcoHY4ScYoV79A4PLvf9LiLL3LqsoeYRpegxMdvVhyDnXcDLLmkLE3kRQDqiRGDW2wvnXgL+fjzYL0p95qxanec6WFfogz3OaK83adokw/erCFXgtUDuEPEMS5rdde5jFb70ZVMp3u0tM63qB+UfBVDiBDMnbrhwD+/hMnQovOITTZekN95ZTjWMT/JYxg0WEBbsm26WB59l16sGk5AHwCx4+cRco1l6Hg6zQhEeJLZRTB8Ic8knOhAvrec19+QH9un1yB0RiV1KcbjJO1U7NM54OjY8/42xEMqM1omoHnR88CQNT1Fex3oTFj+2ub8qmGicMEGOAOfuMGiHkRmObiyAGo80rbeUroJEWndL7FG1xXI1j+Rv5Zrxp4ATosTIulGq1G5og+bORbetx6fXbWxqB2lEcAGpOSx+BriVApKo+EvrFdlTWRbG01Bf+fBX9c7NlSR79lcyIoQE=',
|
|
referralNumber: '39747322',
|
|
eon: 'S39734693',
|
|
debug: true,
|
|
},
|
|
{
|
|
name: 'Dev',
|
|
hostName: 'www-dev2.safelite.com',
|
|
apiHostname: 'digitalapi.dev.safelite.io',
|
|
serverData: 'IUvCHjQRpYbtCcBWWWLiZFwAeebAcoHY4ScYoV79A4PLvf9LiLL3LqsoeYRpegxMdvVhyDnXcDLLmkLE3kRQDqiRGDW2wvnXgL+fjzYL0p95qxanec6WFfogz3OaK83adokw/erCFXgtUDuEPEMS5rdde5jFb70ZVMp3u0tM63qB+UfBVDiBDMnbrhwD+/hMnQovOITTZekN95ZTjWMT/JYxg0WEBbsm26WB59l16sGk5AHwCx4+cRco1l6Hg6zQhEeJLZRTB8Ic8knOhAvrec19+QH9un1yB0RiV1KcbjJO1U7NM54OjY8/42xEMqM1omoHnR88CQNT1Fex3oTFj+2ub8qmGicMEGOAOfuMGiHkRmObiyAGo80rbeUroJEWndL7FG1xXI1j+Rv5Zrxp4ATosTIulGq1G5og+bORbetx6fXbWxqB2lEcAGpOSx+BriVApKo+EvrFdlTWRbG01Bf+fBX9c7NlSR79lcyIoQE=',
|
|
referralNumber: '39747322',
|
|
eon: 'S39734693',
|
|
debug: true,
|
|
},
|
|
{
|
|
name: 'Test',
|
|
hostName: 'www-test2.safelite.com',
|
|
apiHostname: 'digitalapi.test.safelite.io',
|
|
serverData: 'IUvCHjQRpYbtCcBWWWLiZFwAeebAcoHY4ScYoV79A4PLvf9LiLL3LqsoeYRpegxMdvVhyDnXcDLLmkLE3kRQDqiRGDW2wvnXgL+fjzYL0p95qxanec6WFfogz3OaK83adokw/erCFXgtUDuEPEMS5rdde5jFb70ZVMp3u0tM63qB+UfBVDiBDMnbrhwD+/hMnQovOITTZekN95ZTjWMT/JYxg0WEBbsm26WB59l16sGk5AHwCx4+cRco1l6Hg6zQhEeJLZRTB8Ic8knOhAvrec19+QH9un1yB0RiV1KcbjJO1U7NM54OjY8/42xEMqM1omoHnR88CQNT1Fex3oTFj+2ub8qmGicMEGOAOfuMGiHkRmObiyAGo80rbeUroJEWndL7FG1xXI1j+Rv5Zrxp4ATosTIulGq1G5og+bORbetx6fXbWxqB2lEcAGpOSx+BriVApKo+EvrFdlTWRbG01Bf+fBX9c7NlSR79lcyIoQE=',
|
|
referralNumber: '39747322',
|
|
eon: 'S39734693',
|
|
debug: true,
|
|
},
|
|
{
|
|
name: 'QA',
|
|
hostName: 'www-qa2.safelite.com',
|
|
apiHostname: 'digitalapi.qa.safelite.io',
|
|
serverData: 'IUvCHjQRpYbtCcBWWWLiZFwAeebAcoHY4ScYoV79A4PLvf9LiLL3LqsoeYRpegxMdvVhyDnXcDLLmkLE3kRQDqiRGDW2wvnXgL%2BfjzYL0p95qxanec6WFfogz3OaK83adokw%2FerCFXgtUDuEPEMS5rdde5jFb70ZVMp3u0tM63qB%2BUfBVDiBDMnbrhwD%2B%2FhMnQovOITTZekN95ZTjWMT%2FJYxg0WEBbsm26WB59l16sGk5AHwCx4%2BcRco1l6Hg6zQhEeJLZRTB8Ic8knOhAvrec19%2BQH9un1yB0RiV1KcbjJO1U7NM54OjY8%2F42xEMqM1omoHnR88CQNT1Fex3oTFj%2B2ub8qmGicMEGOAOfuMGiHkRmObiyAGo80rbeUroJEWndL7FG1xXI1j%2BRv5Zrxp4ATosTIulGq1G5og%2BbORbetx6fXbWxqB2lEcAGpOSx%2BBriVApKo%2BEvrFdlTWRbG01MJJwy95iidJHxy32l65UQpfnZNBUfaT02OVwcinhhoT9K5vJ%2BAUD6kJXFeGTIiM87H9IbcmNJXjgPJSAzK7nBX3bogx4ch9P3VMnw477kBDIuTKS9%2BH3pLUkgCEb%2F3rykwXh%2FlLF0YjJ7AQ4MdeCTruIXWntYkBcXrG9r10WFhera8KnKdj3%2FDoSLZAAXIc%2BJQXI9n0Nihtr3hGl8bn1bw4IMWTr7TlfemLkEbXMnIiJc4%2BH8aucXVJo%2FnDcDMexA%3D%3D',
|
|
referralNumber: '39747322',
|
|
eon: 'S39734693',
|
|
debug: false,
|
|
},
|
|
{
|
|
name: 'Prod',
|
|
hostName: 'www.safelite.com',
|
|
apiHostname: 'digitalapi.safelite.io',
|
|
serverData: 'qH7+R9oUdeg2mgO/XKCdmeS3x0hWEEO5u35kOGCIewYrSP3XXySL7H3dCNVNHJAdpg7ioDOczYw6s6G3Eiap9y++G2scrLP03/F4WnNBI7BSPfQa737jtM3MzPm2veBFDjuYoF3S6IKJVPMiwb880z4at829Gevf/wK7bCq505x2RAppRszgtrthuKjorQFy1B/xbhbamU2okqfYnYdsUNfXf48+117vo84Uz2Kh81jSXGLA3Hs2MjuvnFTw+OlJ+KiCaYXAZEPjqrPQdYhNiRUxEBrUEGsvs9Fj8+nIgy5iqovF/sNIHcML8HU2Gy90zU01dDIa/0ezuoaPRfkh9HhX2irvIjO6eGp6xHxxhTYV/N8gLvZ2CDX3qZCQMUqe9fNQK0ON545e2y5juitXWktytQgW3+4zFIpdL7c2nTqHibJ6ipH4ZDKN57aeRToLRCGGqA6aneQaYirsM03oYOKQ6t5I+DRcV4and08pnQkzok/I6t9J9QGkRyDzE3n2a0tB3wMekxBrOIgJA3oRbehglp8sjECCWJ9COlKW85GWU9dreTImW2uJapp+68m5nEWpwzzA4VnyNlFz47FyjVFNE8wptM4RStHIvVd4JjVKzno5ybam8RK0WHvx7X4ItmGugL9f5vsKrsB1DoeKQZmB7NZS7uDLR+xKSREHboamnqHIPDTkfJmSvDClvF55z4/L1G9t+f+KmMlQbBYgMqkel4lnDzbWiL22cpoeFCQDcHUVANsF5rKEUdiJv7wYLwRGPYFZVLfW94EmDoBgF5SAiILtD786Jrb+78T8q6vVt86hV4sV+Sci1GDbtzRQ4GKRQs2SEcsO8sVGgO0opcdbyjpcLrmpJKyU5qZDRQfoQvi6wvqfNm0qdpAqrykeN/XRMHxcr7ycCAbV8Ixg8AMd7dOJGMboUIAGUtvmf4yJ1UvuViaHM5lKaUcMg5iX/B2pxiYB5/J9I6uNZiTKZQ==',
|
|
referralNumber: '276488294',
|
|
eon: 'S276469656',
|
|
debug: false,
|
|
},
|
|
];
|
|
|
|
const hostName = window.location.hostname;
|
|
|
|
const match = environmentData.find(
|
|
data => (data.hostName === hostName)
|
|
);
|
|
return environmentData[environmentData.length - 1];
|
|
return match;
|
|
}
|
|
|
|
function updateAtAGlanceText() {
|
|
const healthyCount = Object.values(healthData).filter(service => (service.status === 'healthy' || service.status === 'degraded')).length;
|
|
const totalCount = Object.values(healthData).length;
|
|
let text = 'Healthy';
|
|
let className = 'stat-pill stat-pill-healthy';
|
|
if (healthyCount < totalCount) {
|
|
text = 'Unhealthy';
|
|
className = 'stat-pill stat-pill-unhealthy';
|
|
}
|
|
|
|
document.getElementById('at-a-glance-text').textContent = text;
|
|
document.getElementById('at-a-glance-text').className = className;
|
|
}
|
|
|
|
// Refresh all endpoints
|
|
async function refreshAllEndpoints() {
|
|
if (isRefreshing) return;
|
|
|
|
isRefreshing = true;
|
|
const refreshBtn = document.getElementById('refreshBtn');
|
|
const refreshText = document.getElementById('refreshText');
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
|
|
refreshBtn.disabled = true;
|
|
refreshText.innerHTML = '<div class="spinner"></div>Refreshing...';
|
|
|
|
// Show overlay instead of clearing content
|
|
overlay.classList.add('active');
|
|
|
|
try {
|
|
healthData = {};
|
|
|
|
// Check all microservices in parallel
|
|
const promises = Object.entries(microservices).map(([serviceName, config]) =>
|
|
checkMicroservice(serviceName, config)
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
|
|
renderDashboard();
|
|
updateLastUpdated();
|
|
|
|
} catch (error) {
|
|
console.error('Error refreshing endpoints:', error);
|
|
document.getElementById('content').innerHTML =
|
|
'<div class="error-message">Failed to refresh endpoints. Please check your network connection and try again.</div>';
|
|
} finally {
|
|
isRefreshing = false;
|
|
refreshBtn.disabled = false;
|
|
refreshText.textContent = 'Refresh All';
|
|
updateAtAGlanceText();
|
|
|
|
// Hide overlay
|
|
overlay.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
// Check a single microservice
|
|
async function checkMicroservice(serviceName, config) {
|
|
healthData[serviceName] = {
|
|
status: 'checking',
|
|
endpoints: []
|
|
};
|
|
|
|
const endpointPromises = config.endpoints.map(endpoint =>
|
|
checkEndpoint(serviceName, config.baseUrl, endpoint)
|
|
);
|
|
|
|
const results = await Promise.allSettled(endpointPromises);
|
|
|
|
healthData[serviceName].endpoints = results.map((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
return result.value;
|
|
} else {
|
|
return {
|
|
...config.endpoints[index],
|
|
url: config.baseUrl + config.endpoints[index].path,
|
|
status: 'error',
|
|
statusCode: 'ERROR',
|
|
responseTime: 0,
|
|
error: result.reason.message,
|
|
requestData: config.endpoints[index].body ? JSON.stringify(config.endpoints[index].body, null, 2) : 'No request body',
|
|
responseData: 'Failed to fetch response'
|
|
};
|
|
}
|
|
});
|
|
|
|
// Determine overall service status
|
|
const endpoints = healthData[serviceName].endpoints;
|
|
const healthyCount = endpoints.filter(ep => !isDegraded(ep) && !isUnhealthy(ep)).length;
|
|
const unhealthyCount = endpoints.filter(ep => isUnhealthy(ep)).length;
|
|
const totalCount = endpoints.length;
|
|
|
|
if (healthyCount === totalCount) {
|
|
healthData[serviceName].status = 'healthy';
|
|
} else if (unhealthyCount > 0) {
|
|
healthData[serviceName].status = 'unhealthy';
|
|
} else {
|
|
healthData[serviceName].status = 'degraded';
|
|
}
|
|
}
|
|
|
|
// Check a single endpoint
|
|
async function checkEndpoint(serviceName, baseUrl, endpoint) {
|
|
const url = baseUrl + endpoint.path;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
const response = await makeApiCall(url, endpoint.method, endpoint.body);
|
|
const endTime = Date.now();
|
|
|
|
return {
|
|
...endpoint,
|
|
url: url,
|
|
status: response.ok ? 'success' : 'error',
|
|
statusCode: response.status,
|
|
responseTime: endTime - startTime,
|
|
requestData: endpoint.body ? JSON.stringify(endpoint.body, null, 2) : 'No request body',
|
|
responseData: typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2),
|
|
headers: response.headers
|
|
};
|
|
} catch (error) {
|
|
const endTime = Date.now();
|
|
return {
|
|
...endpoint,
|
|
url: url,
|
|
status: 'error',
|
|
statusCode: 'ERROR',
|
|
responseTime: endTime - startTime,
|
|
error: error.message,
|
|
requestData: endpoint.body ? JSON.stringify(endpoint.body, null, 2) : 'No request body',
|
|
responseData: 'Failed to fetch response'
|
|
};
|
|
}
|
|
}
|
|
|
|
// Make real API calls to the provided endpoints
|
|
async function makeApiCall(url, method, body) {
|
|
const requestOptions = {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
// Add any additional headers you might need
|
|
// 'Authorization': 'Bearer your-token-here',
|
|
// 'X-API-Key': 'your-api-key-here'
|
|
},
|
|
mode: 'cors'
|
|
// Add credentials if needed for authentication
|
|
// credentials: 'include'
|
|
};
|
|
|
|
// Add body for POST, PUT, PATCH requests
|
|
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
|
requestOptions.body = JSON.stringify(body);
|
|
}
|
|
|
|
const response = await fetch(url, requestOptions);
|
|
|
|
let responseData;
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
// Handle different response types
|
|
if (contentType && contentType.includes('application/json')) {
|
|
try {
|
|
responseData = await response.json();
|
|
} catch (e) {
|
|
responseData = await response.text();
|
|
}
|
|
} else {
|
|
responseData = await response.text();
|
|
}
|
|
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
data: responseData,
|
|
headers: Object.fromEntries(response.headers.entries())
|
|
};
|
|
}
|
|
|
|
// Render the dashboard
|
|
function renderDashboard() {
|
|
renderMicroservices();
|
|
}
|
|
|
|
|
|
// Toggle microservice section collapse/expand
|
|
function toggleMicroservice(serviceName) {
|
|
const content = document.getElementById(`content-${serviceName}`);
|
|
const icon = document.getElementById(`icon-${serviceName}`);
|
|
|
|
if (content && icon) {
|
|
const isExpanded = content.classList.contains('expanded');
|
|
|
|
if (isExpanded) {
|
|
content.classList.remove('expanded');
|
|
icon.classList.remove('expanded');
|
|
} else {
|
|
content.classList.add('expanded');
|
|
icon.classList.add('expanded');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Refresh a single endpoint
|
|
async function refreshEndpoint(serviceName, endpointIndex) {
|
|
const btn = document.getElementById(`refresh-${serviceName}-${endpointIndex}`);
|
|
if (!btn || btn.disabled) return;
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<div class="spinner"></div>Refreshing...';
|
|
|
|
try {
|
|
const serviceConfig = microservices[serviceName];
|
|
if (!serviceConfig) return;
|
|
|
|
const endpoint = serviceConfig.endpoints[endpointIndex];
|
|
if (!endpoint) return;
|
|
|
|
// Check the endpoint
|
|
const result = await checkEndpoint(serviceName, serviceConfig.baseUrl, endpoint);
|
|
|
|
// Update the specific endpoint in healthData
|
|
healthData[serviceName].endpoints[endpointIndex] = result;
|
|
|
|
// Re-render just this microservice to update the display
|
|
const serviceEl = document.getElementById(`service-${serviceName}`);
|
|
if (serviceEl) {
|
|
const wasExpanded = document.getElementById(`content-${serviceName}`)?.classList.contains('expanded');
|
|
const newServiceEl = createMicroserviceElement(serviceName, healthData[serviceName]);
|
|
serviceEl.replaceWith(newServiceEl);
|
|
|
|
// Restore expanded state
|
|
if (wasExpanded) {
|
|
const contentEl = document.getElementById(`content-${serviceName}`);
|
|
const iconEl = document.getElementById(`icon-${serviceName}`);
|
|
if (contentEl && iconEl) {
|
|
contentEl.classList.add('expanded');
|
|
iconEl.classList.add('expanded');
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Error refreshing endpoint ${serviceName}[${endpointIndex}]:`, error);
|
|
} finally {
|
|
updateAtAGlanceText();
|
|
btn.disabled = false;
|
|
btn.innerHTML = '↻ Refresh';
|
|
}
|
|
}
|
|
|
|
// Render microservices
|
|
function renderMicroservices() {
|
|
const content = document.getElementById('content');
|
|
|
|
// Track which microservices are currently expanded
|
|
const expandedServices = new Set();
|
|
Object.keys(healthData).forEach(serviceName => {
|
|
const contentEl = document.getElementById(`content-${serviceName}`);
|
|
if (contentEl && contentEl.classList.contains('expanded')) {
|
|
expandedServices.add(serviceName);
|
|
}
|
|
});
|
|
|
|
content.innerHTML = '';
|
|
|
|
Object.entries(healthData).forEach(([serviceName, serviceData]) => {
|
|
const serviceEl = createMicroserviceElement(serviceName, serviceData);
|
|
content.appendChild(serviceEl);
|
|
|
|
// Restore expanded state if it was previously expanded
|
|
if (expandedServices.has(serviceName)) {
|
|
const contentEl = document.getElementById(`content-${serviceName}`);
|
|
const iconEl = document.getElementById(`icon-${serviceName}`);
|
|
if (contentEl && iconEl) {
|
|
contentEl.classList.add('expanded');
|
|
iconEl.classList.add('expanded');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function isUnhealthy(endpoint) {
|
|
return (endpoint.statusCode >= 500 || endpoint.statusCode === 'ERROR') || (endpoint.responseTime > unhealthyResponseThreshold);
|
|
}
|
|
|
|
function isDegraded(endpoint) {
|
|
return (endpoint.statusCode >= 400 && endpoint.statusCode < 500) || (endpoint.responseTime > degradedResponseThreshold);
|
|
}
|
|
|
|
// Create microservice element
|
|
function createMicroserviceElement(serviceName, serviceData) {
|
|
const serviceEl = document.createElement('div');
|
|
serviceEl.className = 'microservice-group';
|
|
serviceEl.id = `service-${serviceName}`;
|
|
|
|
// Calculate endpoint statistics
|
|
const endpoints = serviceData.endpoints;
|
|
const healthyCount = endpoints.filter(ep => !isDegraded(ep) && !isUnhealthy(ep)).length;
|
|
const degradedCount = endpoints.filter((ep) => !isUnhealthy(ep) && isDegraded(ep)).length;
|
|
const unhealthyCount = endpoints.filter((ep) => isUnhealthy(ep)).length;
|
|
|
|
const headerEl = document.createElement('div');
|
|
headerEl.className = 'microservice-header';
|
|
headerEl.onclick = () => toggleMicroservice(serviceName);
|
|
headerEl.innerHTML = `
|
|
<div class="microservice-header-content">
|
|
<span class="collapse-icon" id="icon-${serviceName}">▶</span>
|
|
<div class="microservice-name">${serviceName}</div>
|
|
</div>
|
|
<div class="microservice-header-right">
|
|
<div class="endpoint-stats">
|
|
<div class="stat-pill stat-pill-healthy${healthyCount === 0 ? ' empty' : ''}">Healthy: ${healthyCount}</div>
|
|
<div class="stat-pill stat-pill-degraded${degradedCount === 0 ? ' empty' : ''}">Degraded: ${degradedCount}</div>
|
|
<div class="stat-pill stat-pill-unhealthy${unhealthyCount === 0 ? ' empty' : ''}">Unhealthy: ${unhealthyCount}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
serviceEl.appendChild(headerEl);
|
|
|
|
// Create collapsible content container
|
|
const contentEl = document.createElement('div');
|
|
contentEl.className = 'microservice-content';
|
|
contentEl.id = `content-${serviceName}`;
|
|
|
|
serviceData.endpoints.forEach((endpoint, index) => {
|
|
const endpointEl = createEndpointElement(endpoint, serviceName, index);
|
|
contentEl.appendChild(endpointEl);
|
|
});
|
|
|
|
serviceEl.appendChild(contentEl);
|
|
|
|
return serviceEl;
|
|
}
|
|
|
|
// Create endpoint element
|
|
function createEndpointElement(endpoint, serviceName, endpointIndex) {
|
|
const endpointEl = document.createElement('div');
|
|
endpointEl.className = 'endpoint';
|
|
|
|
const statusClass = getStatusClass(endpoint.statusCode);
|
|
|
|
const methodClass = `method-${endpoint.method.toLowerCase()}`;
|
|
|
|
endpointEl.innerHTML = `
|
|
<div class="endpoint-header">
|
|
<div class="endpoint-header-left">
|
|
<span class="endpoint-method ${methodClass}">${endpoint.method}</span>
|
|
<span class="endpoint-url">${endpoint.url}</span>
|
|
</div>
|
|
<div class="endpoint-header-right">
|
|
<button class="endpoint-refresh-btn" id="refresh-${serviceName}-${endpointIndex}" onclick="refreshEndpoint('${serviceName}', ${endpointIndex})">↻ Refresh</button>
|
|
<span class="status-code ${statusClass}">${endpoint.statusCode}</span>
|
|
<span class="response-time">${endpoint.responseTime}ms</span>
|
|
</div>
|
|
</div>
|
|
<div class="request-response">
|
|
<div class="data-section">
|
|
<h4>Request Data:</h4>
|
|
<div class="data-content">${endpoint.requestData}</div>
|
|
</div>
|
|
<div class="data-section">
|
|
<h4>Response Data:</h4>
|
|
<div class="data-content">${endpoint.responseData}</div>
|
|
</div>
|
|
${endpoint.headers ? `<div class="data-section"><h4>Response Headers:</h4><div class="data-content">${JSON.stringify(endpoint.headers, null, 2)}</div></div>` : ''}
|
|
${endpoint.error ? `<div class="data-section"><h4>Error:</h4><div class="data-content">${endpoint.error}</div></div>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
return endpointEl;
|
|
}
|
|
|
|
// Get CSS class for status code
|
|
function getStatusClass(statusCode) {
|
|
if (statusCode === 'ERROR') return 'status-error';
|
|
if (statusCode >= 200 && statusCode < 300) return 'status-2xx';
|
|
if (statusCode >= 300 && statusCode < 400) return 'status-3xx';
|
|
if (statusCode >= 400 && statusCode < 500) return 'status-4xx';
|
|
if (statusCode >= 500) return 'status-5xx';
|
|
return 'status-error';
|
|
}
|
|
|
|
// Update last updated timestamp
|
|
function updateLastUpdated() {
|
|
const now = new Date();
|
|
const timeString = now.toLocaleTimeString();
|
|
const dateString = now.toLocaleDateString();
|
|
document.getElementById('lastUpdated').textContent = `Last updated: ${dateString} at ${timeString}`;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |