DigitalConsumer.FixMyGlass/public/static/healthcheck/index.html
2025-12-18 15:27:02 -05:00

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>