From e54b36275b9100304c3420fc4a0620e41d397b35 Mon Sep 17 00:00:00 2001 From: brian-mckeown Date: Sat, 27 Jan 2024 16:11:06 -0500 Subject: [PATCH] Announcement Debug Logs. Fixes #50 --- .../controller/AnnouncementController.java | 89 ++++++++++++++----- src/main/resources/static/script.js | 87 ++++++++++++------ src/main/resources/templates/app.html | 15 +++- 3 files changed, 140 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/myapps/flightdash/controller/AnnouncementController.java b/src/main/java/com/myapps/flightdash/controller/AnnouncementController.java index 8cdca7c..a46778e 100644 --- a/src/main/java/com/myapps/flightdash/controller/AnnouncementController.java +++ b/src/main/java/com/myapps/flightdash/controller/AnnouncementController.java @@ -4,12 +4,17 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import io.github.cdimascio.dotenv.Dotenv; import java.io.File; import java.util.*; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + @RestController @RequestMapping("/api/v1/announcements") @@ -28,6 +33,12 @@ public class AnnouncementController { } weatherbitApiToken = token; } + +private String getCurrentUtcDateTime() { + ZonedDateTime utcNow = ZonedDateTime.now(ZoneOffset.UTC); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss.SSS 'UTC'"); + return utcNow.format(formatter); +} @PostMapping("/universal") @@ -96,15 +107,22 @@ public ResponseEntity fetchLandingAnnouncement(@RequestBody Map weatherResponse = restTemplate.getForEntity(arrivalWeatherUrl, Map.class); - apiCallStatus.append("Weather API Call - Status: ") - .append(weatherResponse.getStatusCode()) + ResponseEntity weatherResponse = null; // Declare before try block + try { + weatherResponse = restTemplate.getForEntity(arrivalWeatherUrl, Map.class); + apiCallStatus.append(getCurrentUtcDateTime()).append(" - Weather API Call - Status: ").append(weatherResponse.getStatusCode()).append("\n"); + } catch (HttpClientErrorException e) { + // This will catch client errors like 401 Unauthorized and provide more details + apiCallStatus.append(getCurrentUtcDateTime()).append(" - Weather API Call - HttpClient Error: ") + .append(e.getStatusCode()) + .append(" - ") + .append(e.getResponseBodyAsString()) .append("\n"); - +} catch (Exception e) { + apiCallStatus.append(getCurrentUtcDateTime()).append(" - Weather API Call - Error: ").append(e.getMessage()).append("\n"); + } // Initialize response variables String weatherDescription = null; Number celsiusTemp = null; @@ -150,7 +168,7 @@ public ResponseEntity fetchLandingAnnouncement(@RequestBody Map> data = (List>) weatherResponse.getBody().get("data"); if (data != null && !data.isEmpty()) { Map weatherData = data.get(0); @@ -444,8 +462,8 @@ else if (announcementType.equals("boarding6")) { // Make the POST request to OpenAI String openAiUrl = "https://api.openai.com/v1/chat/completions"; - ResponseEntity openAiResponse = restTemplate.postForEntity(openAiUrl, entity, Map.class); - apiCallStatus.append("OpenAI GPT API Call - Status: ") + try { ResponseEntity openAiResponse = restTemplate.postForEntity(openAiUrl, entity, Map.class); + apiCallStatus.append(getCurrentUtcDateTime()).append(" - OpenAI GPT API Call - Status: ") .append(openAiResponse.getStatusCode()) .append("\n"); @@ -460,6 +478,18 @@ else if (announcementType.equals("boarding6")) { } } } + } catch (HttpClientErrorException e) { + // This will catch client errors like 401 Unauthorized and provide more details + apiCallStatus.append(getCurrentUtcDateTime()).append(" - OpenAI GPT API Call - HttpClient Error: ") + .append(e.getStatusCode()) + .append(" - ") + .append(e.getResponseBodyAsString()) + .append("\n"); + } catch (Exception e) { + apiCallStatus.append(getCurrentUtcDateTime()).append(" - OpenAI GPT API Call - Error: ") + .append(e.getMessage()) + .append("\n"); + } Map ttsRequestBody = new HashMap<>(); ttsRequestBody.put("model", ttsModel); @@ -468,23 +498,25 @@ else if (announcementType.equals("boarding6")) { HttpEntity> ttsEntity = new HttpEntity<>(ttsRequestBody, headers); String ttsUrl = "https://api.openai.com/v1/audio/speech"; + try { ResponseEntity ttsResponse = restTemplate.postForEntity(ttsUrl, ttsEntity, byte[].class); // Construct the response object - if (ttsResponse.getStatusCode().is2xxSuccessful() && ttsResponse.getBody() != null) { + apiCallStatus.append(getCurrentUtcDateTime()).append(" - OpenAI TTS API Call - Status: ") + .append(ttsResponse.getStatusCode()) + .append("\n"); + if (ttsResponse.getStatusCode().is2xxSuccessful() && ttsResponse.getBody() != null) { + byte[] audioData = ttsResponse.getBody(); + + // Encode audio data to Base64 string + String base64Audio = Base64.getEncoder().encodeToString(audioData); - apiCallStatus.append("OpenAI TTS API Call - Success\n"); - // The response should be the binary data of the mp3 file - byte[] audioData = ttsResponse.getBody(); - // Set up headers for audio response - HttpHeaders audioHeaders = new HttpHeaders(); - audioHeaders.setContentType(MediaType.valueOf("audio/mpeg")); - audioHeaders.setContentDisposition(ContentDisposition.builder("inline").filename("announcement.mp3").build()); + // Create a response map + Map responseBody = new HashMap<>(); + responseBody.put("base64Audio", base64Audio); // Encoded audio + responseBody.put("apiCallLog", apiCallStatus.toString()); // API Call Log - return ResponseEntity.ok() - .headers(audioHeaders) - .contentLength(audioData.length) - .body(new ByteArrayResource(audioData)); + return ResponseEntity.ok(responseBody); // Send back as JSON } else { // Handle error case // Since it's an error condition, construct a JSON response @@ -492,6 +524,21 @@ else if (announcementType.equals("boarding6")) { errorResponse.put("error", apiCallStatus + "\nOpenAI TTS Call: Failed to generate audio from the text"); return ResponseEntity.status(ttsResponse.getStatusCode()).body(new MapResource(errorResponse)); } + } catch (HttpClientErrorException e) { + // This will catch client errors like 401 Unauthorized and provide more details + apiCallStatus.append(getCurrentUtcDateTime()).append(" - OpenAI TTS API Call - HttpClient Error: ") + .append(e.getStatusCode()) + .append(" - ") + .append(e.getResponseBodyAsString()) + .append("\n"); + }catch (Exception e) { + apiCallStatus.append(getCurrentUtcDateTime()).append(" - OpenAI TTS API Call - Error: ") + .append(e.getMessage()) + .append("\n"); + } + } else { + // Handle the case where weather API call was not successful or body is null + apiCallStatus.append(getCurrentUtcDateTime()).append(" - Weather API Call - Failed or no data received\n"); } return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiCallStatus + "\ninternal server error"); diff --git a/src/main/resources/static/script.js b/src/main/resources/static/script.js index b0f7b8e..bacb8fc 100644 --- a/src/main/resources/static/script.js +++ b/src/main/resources/static/script.js @@ -6,7 +6,7 @@ var app = angular.module('checklistApp', []); app.controller('ChecklistController', ['$scope', '$sce', '$timeout', '$http', '$document', '$interval', function($scope, $sce, $timeout, $http, $document, $interval) { - $scope.versionNumber = '1.5.2'; + $scope.versionNumber = '1.5.3'; $scope.state = 'Idle'; $scope.messages = []; @@ -735,7 +735,6 @@ $scope.generateFlightCrew = function() { // Add the crew member to the array $scope.flightCrewArray.push(crewMember); }); - console.log($scope.flightCrewArray); } @@ -1351,7 +1350,6 @@ $scope.deBoardPassengersAndBags = function() { $scope.departureIcao = $scope.flightPlanJSONData.origin.icao_code; $scope.arrivalIcao = $scope.flightPlanJSONData.destination.icao_code; $scope.flightLevelString = $scope.flightPlanJSONData.general.stepclimb_string; - console.log("STEP FLIGHT LEVEL STRING: " + $scope.flightLevelString); $scope.numberOfPassengers = Number($scope.flightPlanJSONData.weights.pax_count); @@ -1406,9 +1404,6 @@ $scope.deBoardPassengersAndBags = function() { $scope.runways = $scope.airportData.runways; $scope.frequencies = $scope.airportData.freqs; - console.log($scope.airportData); - console.log($scope.airportData.freqs); - console.log($scope.airportData.runways); $scope.parseMetar($scope.airportInfo.metar); $scope.sortRunwaysByHeadwind(); @@ -1624,26 +1619,39 @@ $scope.deBoardPassengersAndBags = function() { aircraftName: $scope.aircraftName }; - // Define the headers for your POST request - var config = { - headers : { - 'Content-Type': 'application/json' - } - }; + // Create a string representation of requestData with masked API key + var requestDataString = JSON.stringify({ + ...requestData, + openAiApiKey: 'xxxxxxxxx' + }, null, 2); // Format with 2-space indentation for readability + $http({ method: 'POST', url: '/api/v1/announcements/universal', data: requestData, - responseType: 'blob', // Correct way to set the expected response type for binary data headers: { 'Content-Type': 'application/json' }, - }) - .then(function(response) { - var blob = new Blob([response.data], { type: 'audio/mpeg' }); - $scope.audioSrc = $sce.trustAsResourceUrl(URL.createObjectURL(blob)); + transformResponse: function(data, headersGetter) { + try { + // Try to parse the response as JSON + return angular.fromJson(data); + } catch (e) { + // If parsing fails, return the raw response + return data; + } + } + }).then(function(response) { + // Decode the Base64 audio string to a Blob + var audioBlob = base64ToBlob(response.data.base64Audio, 'audio/mpeg'); + $scope.audioSrc = $sce.trustAsResourceUrl(URL.createObjectURL(audioBlob)); - // Use the reusable function + // Assign the API call log + $scope.announcementApiReport = response.data.apiCallLog; + // Append requestDataString to announcementApiReport + $scope.announcementApiReport += "\n\nRequest Data:\n" + requestDataString; + + // Play the audio $scope.playDingThenCallback(function() { var announcementAudio = document.getElementById('announcementAudio'); if (announcementAudio) { @@ -1653,16 +1661,41 @@ $scope.deBoardPassengersAndBags = function() { console.error('Announcement audio element not found'); } }); - }) - .catch(function(error) { - // Handle errors here, such as displaying a message to the user - console.error('Error fetching announcement:', error); - $scope.announcementApiReport = error; - }) - .finally(function() { - $scope.isAnnouncementLoading = false; // Hide spinner - }); + }).catch(function(error) { + console.error('Error fetching announcement:', error); + + // Check if error.data is a string and not empty + if (typeof error.data === 'string' && error.data.trim()) { + $scope.announcementApiReport = error.data; + $scope.announcementApiReport += "\n\nRequest Data:\n" + requestDataString; + } else { + $scope.announcementApiReport = "Unknown error"; + $scope.announcementApiReport += "\n\nRequest Data:\n" + requestDataString; + } + }).finally(function() { + $scope.isAnnouncementLoading = false; + }); }; + + // Helper function to convert base64 string to Blob + function base64ToBlob(base64, contentType) { + var byteCharacters = atob(base64); + var byteArrays = []; + + for (var offset = 0; offset < byteCharacters.length; offset += 512) { + var slice = byteCharacters.slice(offset, offset + 512); + + var byteNumbers = new Array(slice.length); + for (var i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + var byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + return new Blob(byteArrays, {type: contentType}); + } $scope.startPreBoardingAnnouncement = function() { diff --git a/src/main/resources/templates/app.html b/src/main/resources/templates/app.html index 8499ea9..19dfd21 100644 --- a/src/main/resources/templates/app.html +++ b/src/main/resources/templates/app.html @@ -1258,9 +1258,18 @@

Announcements (Open Beta)

report the issue via Support
-

- -

+
+ +
+ +
+ + +