1import { 2 MapInteractionService 3} from "./chunk-4Y5EMHGE.js"; 4import { 5 DeviceIdentityService, 6 isRecordingTooLarge 7} from "./chunk-GEY5NERW.js"; 8import { 9 MapRenderingDiagnosticsService, 10 NavigationService, 11 attachmentToBlob, 12 normalizeScreenshotMimeType, 13 screenshotExtension 14} from "./chunk-CZMIP4ZQ.js"; 15import { 16 APP_VERSION 17} from "./chunk-ODH7QDZO.js"; 18import { 19 BeaconLocationService, 20 LocationService 21} from "./chunk-K52XM66R.js"; 22import { 23 BeaconService 24} from "./chunk-UZKEIBGO.js"; 25import { 26 LocationSource 27} from "./chunk-2E4THSOE.js"; 28import { 29 GPSService 30} from "./chunk-JISY7VJD.js"; 31import { 32 SettingsService 33} from "./chunk-QI7JV4VS.js"; 34import { 35 ApiUrlService 36} from "./chunk-MAIAEXGG.js"; 37import { 38 HttpClient, 39 Injectable, 40 Router, 41 catchError, 42 combineLatest, 43 from, 44 map, 45 of, 46 setClassMetadata, 47 switchMap, 48 ɵɵdefineInjectable, 49 ɵɵinject 50} from "./chunk-UQ6LLOJ6.js"; 51import { 52 __async 53} from "./chunk-CO64IJQE.js"; 54
55// src/app/services/bug-report/recording-attachment.ts 56function capturedRecordingFromRecording(recording, filename) { 57 var _a, _b, _c; 58 const json = JSON.stringify(recording); 59 const byteSize = new TextEncoder().encode(json).length; 60 const eventCounts = { gps: 0, bluetooth: 0, marker: 0 }; 61 for (const event of (_a = recording.events) != null ? _a : []) { 62 if (event.type === "gps") { 63 eventCounts.gps++; 64 } else if (event.type === "bluetooth") { 65 eventCounts.bluetooth++; 66 } else if (event.type === "marker") { 67 eventCounts.marker++; 68 } 69 } 70 return { 71 json, 72 filename, 73 byteSize, 74 durationMs: (_c = (_b = recording.metadata) == null ? void 0 : _b.duration) != null ? _c : 0, 75 eventCounts 76 }; 77} 78
79// src/app/services/bug-report/bug-report.service.ts 80var _BugReportService = class _BugReportService { 81 constructor(http, apiUrlService, router, navigationService, locationService, beaconLocationService, mapInteractionService, beaconService, gpsService, settingsService, renderingDiagnostics, deviceIdentityService) { 82 this.http = http; 83 this.apiUrlService = apiUrlService; 84 this.router = router; 85 this.navigationService = navigationService; 86 this.locationService = locationService; 87 this.beaconLocationService = beaconLocationService; 88 this.mapInteractionService = mapInteractionService; 89 this.beaconService = beaconService; 90 this.gpsService = gpsService; 91 this.settingsService = settingsService; 92 this.renderingDiagnostics = renderingDiagnostics; 93 this.deviceIdentityService = deviceIdentityService; 94 this.apiBaseUrl = this.apiUrlService.getSelectedUrl() + "/api/v1"; 95 this.apiUrlService.selectedUrl$.subscribe((url) => { 96 this.apiBaseUrl = url + "/api/v1"; 97 }); 98 } 99 /** 100 * Upload a screenshot for the bug report. 101 */ 102 uploadScreenshot(screenshot) { 103 const sourceBlob = attachmentToBlob(screenshot); 104 const mimeType = normalizeScreenshotMimeType(sourceBlob.type || screenshot.mimeType); 105 const blob = sourceBlob.type === mimeType ? sourceBlob : new Blob([sourceBlob], { type: mimeType }); 106 const filename = `screenshot.${screenshotExtension(mimeType)}`; 107 const formData = new FormData(); 108 formData.append("file", blob, filename); 109 return this.http.post(`${this.apiBaseUrl}/BugReport/screenshot`, formData); 110 } 111 /** 112 * Upload a signal recording attachment for the bug report. 113 */ 114 uploadRecording(recording) { 115 const blob = new Blob([recording.json], { type: "application/json" }); 116 const filename = recording.filename.endsWith(".json") ? recording.filename : `${recording.filename}.json`; 117 const formData = new FormData(); 118 formData.append("file", blob, filename); 119 return this.http.post(`${this.apiBaseUrl}/BugReport/recording`, formData); 120 } 121 /** 122 * Submit a bug report. 123 */ 124 submitReport(request) { 125 return this.http.post(`${this.apiBaseUrl}/BugReport`, request); 126 } 127 /** 128 * Get device info including a persistent device identifier. 129 * Delegates to the shared DeviceIdentityService so all anonymous uploads 130 * (bug reports, standalone recordings) report the same identifier. 131 */ 132 getDeviceInfo() { 133 return __async(this, null, function* () { 134 return this.deviceIdentityService.getDeviceInfo(); 135 }); 136 } 137 /** 138 * Collect comprehensive technical data from various services. 139 * Includes beacon signals, GPS status, confidence levels, and positioning state. 140 */ 141 collectTechnicalData() { 142 var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w; 143 const navState = this.navigationService.currentState; 144 const beaconLocation = this.locationService.currentBeaconLocation; 145 const locationMode = this.locationService.mode; 146 const beaconModeStr = locationMode === "beacon" || locationMode === "dual" ? locationMode : "unknown"; 147 const mapState = (_c = (_b = (_a = this.mapInteractionService).getCurrentState) == null ? void 0 : _b.call(_a)) != null ? _c : { 148 isManualOverride: false 149 }; 150 const beaconSnapshot = this.beaconService.getBeaconDebugSnapshot(); 151 const timeSinceLastBeacon = this.beaconLocationService.getTimeSinceLastBeaconMs(); 152 const signalQuality = this.calculateSignalQuality(beaconSnapshot); 153 const kalmanState = this.beaconLocationService.getKalmanDebugState(); 154 const stablePair = this.beaconLocationService.getStablePairDebugState(); 155 const gpsDebug = this.getGPSDebugState(); 156 const confidenceState = this.getConfidenceState(); 157 const sourceVoting = this.getSourceVotingState(); 158 const settings = this.getSettingsSnapshot(); 159 return { 160 navigation: { 161 mode: (_d = navState == null ? void 0 : navState.mode) != null ? _d : "idle", 162 targetType: (_e = navState == null ? void 0 : navState.target) == null ? void 0 : _e.type, 163 targetId: (_f = navState == null ? void 0 : navState.target) == null ? void 0 : _f.id, 164 targetName: (_g = navState == null ? void 0 : navState.target) == null ? void 0 : _g.name, 165 distanceRemaining: navState == null ? void 0 : navState.distanceToDestination, 166 currentDirection: (_h = navState == null ? void 0 : navState.currentDirection) == null ? void 0 : _h.instruction, 167 isOffRoute: (_i = navState == null ? void 0 : navState.isOffRoute) != null ? _i : false 168 }, 169 position: { 170 x: (_j = beaconLocation == null ? void 0 : beaconLocation.point) == null ? void 0 : _j.x, 171 y: (_k = beaconLocation == null ? void 0 : beaconLocation.point) == null ? void 0 : _k.y, 172 floorId: (_m = (_l = beaconLocation == null ? void 0 : beaconLocation.floor) == null ? void 0 : _l.entity) == null ? void 0 : _m.id, 173 floorName: (_p = (_o = (_n = beaconLocation == null ? void 0 : beaconLocation.floor) == null ? void 0 : _n.entity) == null ? void 0 : _o.name) != null ? _p : void 0, 174 buildingId: (_r = (_q = beaconLocation == null ? void 0 : beaconLocation.building) == null ? void 0 : _q.entity) == null ? void 0 : _r.id, 175 buildingName: (_u = (_t = (_s = beaconLocation == null ? void 0 : beaconLocation.building) == null ? void 0 : _s.entity) == null ? void 0 : _t.name) != null ? _u : void 0, 176 confidence: beaconLocation == null ? void 0 : beaconLocation.confidence, 177 source: beaconLocation ? "beacon" : "none", 178 activeSource: confidenceState.activeSource, 179 kalmanState, 180 stablePair 181 }, 182 confidence: confidenceState, 183 beacons: { 184 count: beaconSnapshot.length, 185 nearestSignalStrength: (_v = beaconSnapshot[0]) == null ? void 0 : _v.rssi, 186 mode: beaconModeStr, 187 timeSinceLastMs: timeSinceLastBeacon, 188 signalQuality, 189 activeBeacons: beaconSnapshot 190 }, 191 gps: gpsDebug, 192 sourceVoting, 193 map: { 194 isManualOverride: (_w = mapState.isManualOverride) != null ? _w : false, 195 currentScale: void 0, 196 rotation: void 0 197 }, 198 settings, 199 route: this.router.url, 200 appVersion: APP_VERSION, 201 timestamp: (/* @__PURE__ */ new Date()).toISOString(), 202 rendering: this.renderingDiagnostics.getCompactSnapshot() 203 }; 204 } 205 // ============================================================================= 206 // Helper methods for debug data collection 207 // ============================================================================= 208 /** 209 * Get GPS debug state including status and position. 210 */ 211 getGPSDebugState() { 212 const gpsStatus = this.gpsService.currentGpsStatus; 213 const gpsPosition = this.gpsService.currentPosition; 214 return { 215 status: { 216 available: gpsStatus.available, 217 recovering: gpsStatus.recovering, 218 retryAttempt: gpsStatus.retryAttempt, 219 lastError: gpsStatus.lastError 220 }, 221 position: gpsPosition ? { 222 latitude: gpsPosition.latitude, 223 longitude: gpsPosition.longitude, 224 accuracyMeters: gpsPosition.accuracy, 225 altitude: gpsPosition.altitude, 226 heading: gpsPosition.heading, 227 speed: gpsPosition.speed 228 } : null, 229 timeSinceLastMs: (gpsPosition == null ? void 0 : gpsPosition.recordedAt) ? Date.now() - gpsPosition.recordedAt.getTime() : null 230 }; 231 } 232 /** 233 * Get dual confidence state comparing beacon and GPS. 234 */ 235 getConfidenceState() { 236 var _a, _b, _c, _d, _e; 237 const beaconLoc = this.locationService.currentBeaconLocation; 238 const sourceVoting = this.locationService.sourceVotingState; 239 const beaconRaw = (_a = beaconLoc == null ? void 0 : beaconLoc.confidence) != null ? _a : 0; 240 const beaconAdj = (_b = beaconLoc == null ? void 0 : beaconLoc.confidenceAdjusted) != null ? _b : beaconRaw; 241 const gpsLoc = (_c = this.locationService._gpsLocation$) == null ? void 0 : _c.value;
242 const gpsRaw = (_d = gpsLoc == null ? void 0 : gpsLoc.confidence) != null ? _d : 0; 243 const gpsAdj = (_e = gpsLoc == null ? void 0 : gpsLoc.confidenceAdjusted) != null ? _e : gpsRaw; 244 let activeSource = "none"; 245 if (sourceVoting.currentSource === LocationSource.BEACON) { 246 activeSource = "beacon"; 247 } else if (sourceVoting.currentSource === LocationSource.GPS) { 248 activeSource = "gps"; 249 } 250 return { 251 beacon: beaconLoc ? { 252 raw: beaconRaw, 253 adjusted: beaconAdj 254 } : null, 255 gps: gpsLoc ? { 256 raw: gpsRaw, 257 adjusted: gpsAdj 258 } : null, 259 delta: Math.abs(beaconAdj - gpsAdj), 260 activeSource 261 }; 262 } 263 /** 264 * Get source voting state for beacon/GPS switching. 265 */ 266 getSourceVotingState() { 267 const voting = this.locationService.sourceVotingState; 268 let targetSource = null; 269 if (voting.targetSource === LocationSource.BEACON) { 270 targetSource = "beacon"; 271 } else if (voting.targetSource === LocationSource.GPS) { 272 targetSource = "gps"; 273 } 274 let currentSource = "beacon"; 275 if (voting.currentSource === LocationSource.GPS) { 276 currentSource = "gps"; 277 } 278 return { 279 isVoting: voting.isVoting, 280 progress: voting.progress, 281 targetSource, 282 currentSource, 283 timeElapsedMs: voting.timeElapsed, 284 timeRequiredMs: voting.timeRequired 285 }; 286 } 287 /** 288 * Calculate signal quality metrics from beacon snapshots. 289 */ 290 calculateSignalQuality(beacons) { 291 if (beacons.length === 0) 292 return null; 293 const rssiValues = beacons.map((b) => b.rssi); 294 return { 295 avgRssi: rssiValues.reduce((a, b) => a + b, 0) / rssiValues.length, 296 strongestRssi: Math.max(...rssiValues), 297 weakestRssi: Math.min(...rssiValues) 298 }; 299 } 300 /** 301 * Get settings snapshot at bug report time. 302 */ 303 getSettingsSnapshot() { 304 var _a, _b, _c; 305 const settings = this.settingsService.currentSettings; 306 return { 307 positioningMode: settings.positioningMode || "STABLE", 308 rssiFilterType: settings.rssiFilterType || "simpleEma", 309 kalmanEnabled: (_a = settings.enableGraphKalmanFilter) != null ? _a : true, 310 beaconDebugEnabled: (_b = settings.enableBeaconDebugLogging) != null ? _b : false, 311 smartSwitchingEnabled: (_c = settings.enableSmartSourceSwitching) != null ? _c : true, 312 locationMode: this.locationService.mode 313 }; 314 } 315 /** 316 * Get current campus ID if available. 317 */ 318 getCurrentCampusId() { 319 var _a, _b, _c; 320 return (_c = (_b = (_a = this.locationService.currentBeaconLocation) == null ? void 0 : _a.campus) == null ? void 0 : _b.entity) == null ? void 0 : _c.id; 321 } 322 /** 323 * Build a complete bug report request. 324 */ 325 buildReportRequest(description, category, screenshotUploadId, technicalData, recordingUploadId) { 326 return __async(this, null, function* () { 327 const deviceInfo = yield this.getDeviceInfo(); 328 const techData = technicalData != null ? technicalData : this.collectTechnicalData(); 329 return { 330 description, 331 category, 332 deviceIdentifier: deviceInfo.deviceIdentifier, 333 deviceModel: deviceInfo.model, 334 osVersion: deviceInfo.osVersion, 335 appVersion: deviceInfo.appVersion, 336 screenshotUploadId, 337 recordingUploadId, 338 technicalDataJson: JSON.stringify(techData), 339 campusId: this.getCurrentCampusId() 340 }; 341 }); 342 } 343 /** 344 * Submit a complete bug report (screenshot upload + recording upload + report creation). 345 * 346 * Attachment uploads are best-effort: if the screenshot or recording upload 347 * fails the report is still submitted without that attachment. Recording 348 * attachments that exceed the upload cap are never attempted. 349 */ 350 submitCompleteReport(description, category, screenshot, technicalData, recording) { 351 const reportTechnicalData = technicalData != null ? technicalData : this.collectTechnicalData(); 352 const screenshotUpload$ = screenshot ? this.uploadScreenshot(screenshot).pipe(map((upload) => upload == null ? void 0 : upload.id), catchError((error) => { 353 console.warn("Screenshot upload failed, submitting without screenshot:", error); 354 return of(void 0); 355 })) : of(void 0);
356 const canUploadRecording = !!recording && !isRecordingTooLarge(recording); 357 if (recording && !canUploadRecording) { 358 console.warn(`[BugReportService] Recording ${recording.filename} exceeds the upload cap; submitting without it`); 359 } 360 const recordingUpload$ = canUploadRecording ? this.uploadRecording(recording).pipe(map((upload) => upload == null ? void 0 : upload.id), catchError((error) => { 361 console.warn("Signal recording upload failed, submitting without recording:", error); 362 return of(void 0); 363 })) : of(void 0); 364 return combineLatest([screenshotUpload$, recordingUpload$]).pipe(switchMap(([screenshotUploadId, recordingUploadId]) => from(this.buildReportRequest(description, category, screenshotUploadId, reportTechnicalData, recordingUploadId))), switchMap((request) => this.submitReport(request))); 365 } 366 // ============================================ 367 // Admin API methods 368 // ============================================ 369 /** 370 * Get paginated list of bug reports (Admin only). 371 */ 372 getReportList(params = {}) { 373 const queryParams = new URLSearchParams(); 374 if (params.status) 375 queryParams.set("status", params.status); 376 if (params.category) 377 queryParams.set("category", params.category); 378 if (params.campusId) 379 queryParams.set("campusId", params.campusId); 380 if (params.deviceIdentifier) 381 queryParams.set("deviceIdentifier", params.deviceIdentifier); 382 if (params.page) 383 queryParams.set("page", params.page.toString()); 384 if (params.pageSize) 385 queryParams.set("pageSize", params.pageSize.toString()); 386 const queryString = queryParams.toString(); 387 const url = `${this.apiBaseUrl}/BugReport${queryString ? "?" + queryString : ""}`; 388 return this.http.get(url); 389 } 390 /** 391 * Get a single bug report by ID (Admin only). 392 */ 393 getReport(id) { 394 return this.http.get(`${this.apiBaseUrl}/BugReport/${id}`); 395 } 396 /** 397 * Update bug report status (Admin only). 398 */ 399 updateReportStatus(id, status, adminNotes) { 400 return this.http.patch(`${this.apiBaseUrl}/BugReport/${id}/status`, { 401 status, 402 adminNotes 403 }); 404 } 405 /** 406 * Delete a bug report (Admin only). 407 */ 408 deleteReport(id) { 409 return this.http.delete(`${this.apiBaseUrl}/BugReport/${id}`); 410 } 411 /** 412 * Get bug report statistics (Admin only). 413 */ 414 getStats() { 415 return this.http.get(`${this.apiBaseUrl}/BugReport/stats`); 416 } 417}; 418_BugReportService.\u0275fac = function BugReportService_Factory(__ngFactoryType__) { 419 return new (__ngFactoryType__ || _BugReportService)(\u0275\u0275inject(HttpClient), \u0275\u0275inject(ApiUrlService), \u0275\u0275inject(Router), \u0275\u0275inject(NavigationService), \u0275\u0275inject(LocationService), \u0275\u0275inject(BeaconLocationService), \u0275\u0275inject(MapInteractionService), \u0275\u0275inject(BeaconService), \u0275\u0275inject(GPSService), \u0275\u0275inject(SettingsService), \u0275\u0275inject(MapRenderingDiagnosticsService), \u0275\u0275inject(DeviceIdentityService)); 420}; 421_BugReportService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _BugReportService, factory: _BugReportService.\u0275fac, providedIn: "root" }); 422var BugReportService = _BugReportService; 423(() => { 424 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(BugReportService, [{ 425 type: Injectable, 426 args: [{ 427 providedIn: "root" 428 }] 429 }], () => [{ type: HttpClient }, { type: ApiUrlService }, { type: Router }, { type: NavigationService }, { type: LocationService }, { type: BeaconLocationService }, { type: MapInteractionService }, { type: BeaconService }, { type: GPSService }, { type: SettingsService }, { type: MapRenderingDiagnosticsService }, { type: DeviceIdentityService }], null); 430})(); 431
432// src/app/services/bug-report/bug-report.model.ts 433var BUG_REPORT_CATEGORIES = [ 434 { value: "navigation", labelKey: "bugReport.category.navigation", icon: "lucideNavigation" }, 435 { value: "positioning", labelKey: "bugReport.category.positioning", icon: "lucideMapPin" }, 436 { value: "map_display", labelKey: "bugReport.category.mapDisplay", icon: "lucideMap" }, 437 { value: "performance", labelKey: "bugReport.category.performance", icon: "lucideGauge" }, 438 { value: "ui", labelKey: "bugReport.category.ui", icon: "lucideLayoutDashboard" }, 439 { value: "other", labelKey: "bugReport.category.other", icon: "lucideCircleHelp" } 440]; 441var BUG_REPORT_STATUSES = [ 442 { value: "new", labelKey: "bugReport.status.new" }, 443 { value: "acknowledged", labelKey: "bugReport.status.acknowledged" }, 444 { value: "in_progress", labelKey: "bugReport.status.inProgress" }, 445 { value: "resolved", labelKey: "bugReport.status.resolved" }, 446 { value: "closed", labelKey: "bugReport.status.closed" } 447]; 448 449export { 450 capturedRecordingFromRecording, 451 BugReportService, 452 BUG_REPORT_CATEGORIES, 453 BUG_REPORT_STATUSES 454};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.