1071 lines
No EOL
50 KiB
HTML
1071 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/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> |