Add severity to UI logs

This commit is contained in:
aditya.chandel
2025-10-15 23:59:20 -06:00
parent c8a9075afb
commit d9528af522
15 changed files with 106 additions and 58 deletions

View File

@@ -10,12 +10,26 @@ public class LogNotification {
private final Instant timestamp = Instant.now();
private final String message;
private final Severity severity;
public LogNotification(String message) {
public LogNotification(String message, Severity severity) {
this.message = message;
this.severity = severity;
}
public static LogNotification createLogNotification(String message) {
return new LogNotification(message);
public static LogNotification createLogNotification(String message, Severity severity) {
return new LogNotification(message, severity);
}
public static LogNotification info(String message) {
return new LogNotification(message, Severity.INFO);
}
public static LogNotification warn(String message) {
return new LogNotification(message, Severity.WARN);
}
public static LogNotification error(String message) {
return new LogNotification(message, Severity.ERROR);
}
}

View File

@@ -0,0 +1,8 @@
package com.adityachandel.booklore.model.websocket;
public enum Severity {
INFO,
WARN,
ERROR
}

View File

@@ -20,7 +20,6 @@ import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -106,7 +105,7 @@ public class BookdropEventHandlerService {
int queueSize = fileQueue.size();
notificationService.sendMessageToPermissions(
Topic.LOG,
new LogNotification("Processing bookdrop file: " + fileName + " (" + queueSize + " files remaining)"),
LogNotification.info("Processing bookdrop file: " + fileName + " (" + queueSize + " files remaining)"),
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY)
);
@@ -134,13 +133,13 @@ public class BookdropEventHandlerService {
if (fileQueue.isEmpty()) {
notificationService.sendMessageToPermissions(
Topic.LOG,
new LogNotification("All bookdrop files have finished processing"),
LogNotification.info("All bookdrop files have finished processing"),
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY)
);
} else {
notificationService.sendMessageToPermissions(
Topic.LOG,
new LogNotification("Finished processing bookdrop file: " + fileName + " (" + fileQueue.size() + " files remaining)"),
LogNotification.info("Finished processing bookdrop file: " + fileName + " (" + fileQueue.size() + " files remaining)"),
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY)
);
}

View File

@@ -5,6 +5,8 @@ import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.EmailProviderEntity;
import com.adityachandel.booklore.model.entity.EmailRecipientEntity;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Severity;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.repository.EmailProviderRepository;
@@ -54,17 +56,17 @@ public class EmailService {
private void sendEmailInVirtualThread(EmailProviderEntity emailProvider, String recipientEmail, BookEntity book) {
String bookTitle = book.getMetadata().getTitle();
String logMessage = "Email dispatch initiated for book: " + bookTitle + " to " + recipientEmail;
notificationService.sendMessage(Topic.LOG, createLogNotification(logMessage));
notificationService.sendMessage(Topic.LOG, LogNotification.info(logMessage));
log.info(logMessage);
SecurityContextVirtualThread.runWithSecurityContext(() -> {
try {
sendEmail(emailProvider, recipientEmail, book);
String successMessage = "The book: " + bookTitle + " has been successfully sent to " + recipientEmail;
notificationService.sendMessage(Topic.LOG, createLogNotification(successMessage));
notificationService.sendMessage(Topic.LOG, LogNotification.info(successMessage));
log.info(successMessage);
} catch (Exception e) {
String errorMessage = "An error occurred while sending the book: " + bookTitle + " to " + recipientEmail + ". Error: " + e.getMessage();
notificationService.sendMessage(Topic.LOG, createLogNotification(errorMessage));
notificationService.sendMessage(Topic.LOG, LogNotification.error(errorMessage));
log.error(errorMessage, e);
}
});

View File

@@ -7,6 +7,7 @@ import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.EmailProviderV2Entity;
import com.adityachandel.booklore.model.entity.EmailRecipientV2Entity;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.repository.EmailProviderV2Repository;
@@ -62,17 +63,17 @@ public class SendEmailV2Service {
private void sendEmailInVirtualThread(EmailProviderV2Entity emailProvider, String recipientEmail, BookEntity book) {
String bookTitle = book.getMetadata().getTitle();
String logMessage = "Email dispatch initiated for book: " + bookTitle + " to " + recipientEmail;
notificationService.sendMessage(Topic.LOG, createLogNotification(logMessage));
notificationService.sendMessage(Topic.LOG, LogNotification.info(logMessage));
log.info(logMessage);
SecurityContextVirtualThread.runWithSecurityContext(() -> {
try {
sendEmail(emailProvider, recipientEmail, book);
String successMessage = "The book: " + bookTitle + " has been successfully sent to " + recipientEmail;
notificationService.sendMessage(Topic.LOG, createLogNotification(successMessage));
notificationService.sendMessage(Topic.LOG, LogNotification.info(successMessage));
log.info(successMessage);
} catch (Exception e) {
String errorMessage = "An error occurred while sending the book: " + bookTitle + " to " + recipientEmail + ". Error: " + e.getMessage();
notificationService.sendMessage(Topic.LOG, createLogNotification(errorMessage));
notificationService.sendMessage(Topic.LOG, LogNotification.error(errorMessage));
log.error(errorMessage, e);
}
});

View File

@@ -1,6 +1,7 @@
package com.adityachandel.booklore.service.event;
import com.adityachandel.booklore.model.dto.BookLoreUser;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.service.user.UserService;
import lombok.AllArgsConstructor;
@@ -24,7 +25,7 @@ public class AdminEventBroadcaster {
.filter(u -> u.getPermissions().isAdmin())
.toList();
for (BookLoreUser admin : admins) {
messagingTemplate.convertAndSendToUser(admin.getUsername(), Topic.LOG.getPath(), createLogNotification(message));
messagingTemplate.convertAndSendToUser(admin.getUsername(), Topic.LOG.getPath(), LogNotification.info(message));
}
}
}

View File

@@ -1,6 +1,7 @@
package com.adityachandel.booklore.service.event;
import com.adityachandel.booklore.model.dto.Book;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.service.user.UserService;
import lombok.AllArgsConstructor;
@@ -26,7 +27,7 @@ public class BookEventBroadcaster {
.forEach(u -> {
String username = u.getUsername();
messagingTemplate.convertAndSendToUser(username, Topic.BOOK_ADD.getPath(), book);
messagingTemplate.convertAndSendToUser(username, Topic.LOG.getPath(), createLogNotification("Book added: " + book.getFileName()));
messagingTemplate.convertAndSendToUser(username, Topic.LOG.getPath(), LogNotification.info("Book added: " + book.getFileName()));
});
}
}

View File

@@ -5,6 +5,7 @@ import com.adityachandel.booklore.model.dto.settings.LibraryFile;
import com.adityachandel.booklore.model.entity.BookAdditionalFileEntity;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.LibraryEntity;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.BookAdditionalFileRepository;
import com.adityachandel.booklore.repository.LibraryRepository;
@@ -42,17 +43,17 @@ public class LibraryProcessingService {
@Transactional
public void processLibrary(long libraryId) throws IOException {
LibraryEntity libraryEntity = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
notificationService.sendMessage(Topic.LOG, createLogNotification("Started processing library: " + libraryEntity.getName()));
notificationService.sendMessage(Topic.LOG, LogNotification.info("Started processing library: " + libraryEntity.getName()));
LibraryFileProcessor processor = fileProcessorRegistry.getProcessor(libraryEntity);
List<LibraryFile> libraryFiles = libraryFileHelper.getLibraryFiles(libraryEntity, processor);
processor.processLibraryFiles(libraryFiles, libraryEntity);
notificationService.sendMessage(Topic.LOG, createLogNotification("Finished processing library: " + libraryEntity.getName()));
notificationService.sendMessage(Topic.LOG, LogNotification.info("Finished processing library: " + libraryEntity.getName()));
}
@Transactional
public void rescanLibrary(RescanLibraryContext context) throws IOException {
LibraryEntity libraryEntity = libraryRepository.findById(context.getLibraryId()).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(context.getLibraryId()));
notificationService.sendMessage(Topic.LOG, createLogNotification("Started refreshing library: " + libraryEntity.getName()));
notificationService.sendMessage(Topic.LOG, LogNotification.info("Started refreshing library: " + libraryEntity.getName()));
LibraryFileProcessor processor = fileProcessorRegistry.getProcessor(libraryEntity);
List<LibraryFile> libraryFiles = libraryFileHelper.getLibraryFiles(libraryEntity, processor);
List<Long> additionalFileIds = detectDeletedAdditionalFiles(libraryFiles, libraryEntity);
@@ -69,7 +70,7 @@ public class LibraryProcessingService {
entityManager.clear();
processor.processLibraryFiles(detectNewBookPaths(libraryFiles, libraryEntity), libraryEntity);
notificationService.sendMessage(Topic.LOG, createLogNotification("Finished refreshing library: " + libraryEntity.getName()));
notificationService.sendMessage(Topic.LOG, LogNotification.info("Finished refreshing library: " + libraryEntity.getName()));
}
public void processLibraryFiles(List<LibraryFile> libraryFiles, LibraryEntity libraryEntity) {

View File

@@ -18,6 +18,7 @@ import com.adityachandel.booklore.model.entity.BookMetadataEntity;
import com.adityachandel.booklore.model.enums.BookFileType;
import com.adityachandel.booklore.model.enums.Lock;
import com.adityachandel.booklore.model.enums.MetadataProvider;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.BookMetadataRepository;
import com.adityachandel.booklore.repository.BookRepository;
@@ -192,7 +193,7 @@ public class BookMetadataService {
.filter(book -> book.getMetadata().getCoverLocked() == null || !book.getMetadata().getCoverLocked())
.toList();
int total = books.size();
notificationService.sendMessage(Topic.LOG, createLogNotification("Started regenerating covers for " + total + " books"));
notificationService.sendMessage(Topic.LOG, LogNotification.info("Started regenerating covers for " + total + " books"));
int[] current = {1};
for (BookEntity book : books) {
@@ -204,11 +205,10 @@ public class BookMetadataService {
}
current[0]++;
}
notificationService.sendMessage(Topic.LOG, createLogNotification("Finished regenerating covers"));
notificationService.sendMessage(Topic.LOG, LogNotification.info("Finished regenerating covers"));
} catch (Exception e) {
log.error("Error during cover regeneration: {}", e.getMessage(), e);
notificationService.sendMessage(Topic.LOG, createLogNotification("Error during cover regeneration: " + e.getMessage()));
notificationService.sendMessage(Topic.LOG, LogNotification.error("Error occurred during cover regeneration"));
}
});
}
@@ -216,7 +216,7 @@ public class BookMetadataService {
private void regenerateCoverForBook(BookEntity book, String progress) {
String title = book.getMetadata().getTitle();
String message = progress + "Regenerating cover for: " + title;
notificationService.sendMessage(Topic.LOG, createLogNotification(message));
notificationService.sendMessage(Topic.LOG, LogNotification.info(message));
BookFileProcessor processor = processorRegistry.getProcessorOrThrow(book.getBookType());
processor.generateCover(book);

View File

@@ -6,8 +6,7 @@ import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.LibraryEntity;
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
import com.adityachandel.booklore.model.enums.BookFileExtension;
import com.adityachandel.booklore.model.enums.BookFileType;
import com.adityachandel.booklore.model.enums.PermissionType;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.LibraryRepository;
import com.adityachandel.booklore.service.NotificationService;
@@ -25,7 +24,6 @@ import java.util.Set;
import static com.adityachandel.booklore.model.enums.PermissionType.ADMIN;
import static com.adityachandel.booklore.model.enums.PermissionType.MANIPULATE_LIBRARY;
import static com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification;
@Slf4j
@Service
@@ -52,7 +50,7 @@ public class BookFileTransactionalHandler {
String fileName = path.getFileName().toString();
String libraryPath = bookFilePersistenceService.findMatchingLibraryPath(libraryEntity, path);
notificationService.sendMessageToPermissions(Topic.LOG, createLogNotification("Started processing file: " + filePath), Set.of(ADMIN, MANIPULATE_LIBRARY));
notificationService.sendMessageToPermissions(Topic.LOG, LogNotification.info("Started processing file: " + filePath), Set.of(ADMIN, MANIPULATE_LIBRARY));
LibraryPathEntity libraryPathEntity = bookFilePersistenceService.getLibraryPathEntityForFile(libraryEntity, libraryPath);
@@ -68,7 +66,7 @@ public class BookFileTransactionalHandler {
libraryProcessingService.processLibraryFiles(List.of(libraryFile), libraryEntity);
notificationService.sendMessageToPermissions(Topic.LOG, createLogNotification("Finished processing file: " + filePath), Set.of(ADMIN, MANIPULATE_LIBRARY));
notificationService.sendMessageToPermissions(Topic.LOG, LogNotification.info("Finished processing file: " + filePath), Set.of(ADMIN, MANIPULATE_LIBRARY));
log.info("[CREATE] Completed processing for file '{}'", filePath);
}
}

View File

@@ -19,7 +19,6 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Editor} from 'primeng/editor';
import {ProgressBar} from 'primeng/progressbar';
import {MetadataRefreshType} from '../../../model/request/metadata-refresh-type.enum';
import {MetadataRefreshRequest} from '../../../model/request/metadata-refresh-request.model';
import {Router} from '@angular/router';
import {filter, map, switchMap, take, tap} from 'rxjs/operators';
import {Menu} from 'primeng/menu';
@@ -38,7 +37,6 @@ import {BookDialogHelperService} from '../../../../book/components/book-browser/
import {TagColor, TagComponent} from '../../../../../shared/components/tag/tag.component';
import {MetadataFetchOptionsComponent} from '../../metadata-options-dialog/metadata-fetch-options/metadata-fetch-options.component';
import {BookNotesComponent} from '../../../../book/components/book-notes/book-notes-component';
import {TaskCreateRequest, TaskService, TaskType} from '../../../../settings/task-management/task.service';
import {TaskHelperService} from '../../../../settings/task-management/task-helper.service';
@Component({

View File

@@ -1,4 +1,11 @@
<div class="flex flex-col p-4 space-y-2 live-border">
<p class="text-xs text-zinc-400">{{ latestNotification.timestamp }}</p>
<div class="flex flex-row items-center gap-2">
@if (latestNotification.severity) {
<app-tag [color]="getSeverityColor(latestNotification.severity)" size="3xs" class="self-center">
{{ latestNotification.severity }}
</app-tag>
}
<span class="text-xs text-zinc-400 self-center">{{ latestNotification.timestamp }}</span>
</div>
<p class="font-normal text-zinc-200">{{ latestNotification.message }}</p>
</div>

View File

@@ -1,6 +1,9 @@
import {Component, inject} from '@angular/core';
import {NotificationEventService} from '../../websocket/notification-event.service';
import {LogNotification} from '../../websocket/model/log-notification.model';
import {Tag} from 'primeng/tag';
import {NgIf} from '@angular/common';
import {TagComponent} from '../tag/tag.component';
@Component({
selector: 'app-live-notification-box',
@@ -10,6 +13,9 @@ import {LogNotification} from '../../websocket/model/log-notification.model';
host: {
class: 'config-panel'
},
imports: [
TagComponent
]
})
export class LiveNotificationBoxComponent {
latestNotification: LogNotification = {message: 'No recent notifications...'};
@@ -21,4 +27,17 @@ export class LiveNotificationBoxComponent {
this.latestNotification = notification;
});
}
getSeverityColor(severity?: string): 'red' | 'amber' | 'green' | 'gray' {
switch (severity) {
case 'ERROR':
return 'red';
case 'WARN':
return 'amber';
case 'INFO':
return 'green';
default:
return 'gray';
}
}
}

View File

@@ -24,6 +24,7 @@ import {BookdropFileService} from '../../../../features/bookdrop/service/bookdro
import {DialogLauncherService} from '../../../services/dialog-launcher.service';
import {DuplicateFileService} from '../../../websocket/duplicate-file.service';
import {UnifiedNotificationBoxComponent} from '../../../components/unified-notification-popover/unified-notification-popover-component';
import {Severity, LogNotification} from '../../../websocket/model/log-notification.model';
@Component({
selector: 'app-topbar',
@@ -70,6 +71,7 @@ export class AppTopBarComponent implements OnDestroy {
private latestTasks: { [taskId: string]: MetadataBatchProgressNotification } = {};
private latestHasPendingFiles = false;
private latestHasDuplicateFiles = false;
private latestNotificationSeverity?: Severity;
constructor(
public layoutService: LayoutService,
@@ -174,7 +176,8 @@ export class AppTopBarComponent implements OnDestroy {
private subscribeToNotifications() {
this.notificationService.latestNotification$
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
.subscribe((notification: LogNotification) => {
this.latestNotificationSeverity = notification.severity;
this.triggerPulseEffect();
});
}
@@ -227,9 +230,21 @@ export class AppTopBarComponent implements OnDestroy {
}
get iconColor(): string {
if (this.progressHighlight) return 'yellow';
if (this.showPulse) return 'orange';
if (this.completedTaskCount > 0 || this.hasPendingBookdropFiles || this.hasDuplicateFiles) return 'yellowgreen';
if (this.progressHighlight) return 'gold';
if (this.showPulse) {
switch (this.latestNotificationSeverity) {
case Severity.ERROR:
return 'crimson';
case Severity.INFO:
return 'aqua';
case Severity.WARN:
return 'orange';
default:
return 'orange';
}
}
if (this.completedTaskCount > 0 || this.hasPendingBookdropFiles || this.hasDuplicateFiles)
return 'limegreen';
return 'inherit';
}

View File

@@ -1,13 +1,13 @@
export enum TaskStatus {
IN_PROGRESS = 'IN_PROGRESS',
CANCELLED = 'CANCELLED',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED'
export enum Severity {
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR'
}
export interface LogNotification {
timestamp?: string;
message: string;
severity?: Severity;
}
export function parseLogNotification(messageBody: string): LogNotification {
@@ -15,22 +15,6 @@ export function parseLogNotification(messageBody: string): LogNotification {
return {
timestamp: raw.timestamp ? new Date(raw.timestamp).toLocaleTimeString() : undefined,
message: raw.message,
};
}
export interface TaskMessage {
taskId: string;
timestamp: string;
title?: string;
message: string;
cancellable: boolean;
status: TaskStatus;
}
export function parseTaskMessage(messageBody: string): TaskMessage {
const raw = JSON.parse(messageBody) as TaskMessage;
return {
...raw,
timestamp: new Date(raw.timestamp).toLocaleTimeString()
severity: raw.severity ? Severity[raw.severity as keyof typeof Severity] : undefined
};
}