From 0101c0ca2cfea31772344c4d84fbc3d42aae8083 Mon Sep 17 00:00:00 2001 From: Stefan Hardegger Date: Sun, 21 Sep 2025 14:51:21 +0200 Subject: [PATCH] bugfixes, and logging cleanup --- .env.production | 2 +- .../storycove/controller/FileController.java | 7 +- .../storycove/service/EPUBImportService.java | 19 ++-- .../com/storycove/service/ImageService.java | 30 +++--- .../com/storycove/service/LibraryService.java | 36 +++---- .../storycove/service/OpenSearchService.java | 94 +++++++++---------- frontend/src/app/globals.css | 9 ++ frontend/src/app/stories/[id]/page.tsx | 66 ++++++++++--- .../stories/PortableTextEditorNew.tsx | 25 ++--- frontend/src/lib/debug.ts | 90 ++++++++++++++++++ frontend/src/lib/sanitization.ts | 13 +-- 11 files changed, 271 insertions(+), 120 deletions(-) create mode 100644 frontend/src/lib/debug.ts diff --git a/.env.production b/.env.production index e85f2e1..b1acc7d 100644 --- a/.env.production +++ b/.env.production @@ -19,7 +19,7 @@ JWT_SECRET=REPLACE_WITH_SECURE_JWT_SECRET_MINIMUM_32_CHARS APP_PASSWORD=REPLACE_WITH_SECURE_APP_PASSWORD # OpenSearch Configuration -OPENSEARCH_PASSWORD=REPLACE_WITH_SECURE_OPENSEARCH_PASSWORD +#OPENSEARCH_PASSWORD=REPLACE_WITH_SECURE_OPENSEARCH_PASSWORD SEARCH_ENGINE=opensearch # Image Storage diff --git a/backend/src/main/java/com/storycove/controller/FileController.java b/backend/src/main/java/com/storycove/controller/FileController.java index fff2ab1..7824b92 100644 --- a/backend/src/main/java/com/storycove/controller/FileController.java +++ b/backend/src/main/java/com/storycove/controller/FileController.java @@ -2,6 +2,8 @@ package com.storycove.controller; import com.storycove.service.ImageService; import com.storycove.service.LibraryService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -21,6 +23,7 @@ import java.util.Map; @RestController @RequestMapping("/api/files") public class FileController { + private static final Logger log = LoggerFactory.getLogger(FileController.class); private final ImageService imageService; private final LibraryService libraryService; @@ -32,7 +35,7 @@ public class FileController { private String getCurrentLibraryId() { String libraryId = libraryService.getCurrentLibraryId(); - System.out.println("FileController - Current Library ID: " + libraryId); + log.debug("FileController - Current Library ID: {}", libraryId); return libraryId != null ? libraryId : "default"; } @@ -48,7 +51,7 @@ public class FileController { String imageUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath; response.put("url", imageUrl); - System.out.println("Upload response - path: " + imagePath + ", url: " + imageUrl); + log.debug("Upload response - path: {}, url: {}", imagePath, imageUrl); return ResponseEntity.ok(response); } catch (IllegalArgumentException e) { diff --git a/backend/src/main/java/com/storycove/service/EPUBImportService.java b/backend/src/main/java/com/storycove/service/EPUBImportService.java index be1544e..6f5de6b 100644 --- a/backend/src/main/java/com/storycove/service/EPUBImportService.java +++ b/backend/src/main/java/com/storycove/service/EPUBImportService.java @@ -16,6 +16,8 @@ import nl.siegmann.epublib.epub.EpubReader; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +32,7 @@ import java.util.Optional; @Service @Transactional public class EPUBImportService { + private static final Logger log = LoggerFactory.getLogger(EPUBImportService.class); private final StoryService storyService; private final AuthorService authorService; @@ -87,12 +90,12 @@ public class EPUBImportService { savedStory = storyService.update(savedStory.getId(), savedStory); // Log the image processing results - System.out.println("EPUB Import - Image processing completed for story " + savedStory.getId() + - ". Downloaded " + imageResult.getDownloadedImages().size() + " images."); + log.debug("EPUB Import - Image processing completed for story {}. Downloaded {} images.", + savedStory.getId(), imageResult.getDownloadedImages().size()); if (imageResult.hasWarnings()) { - System.out.println("EPUB Import - Image processing warnings: " + - String.join(", ", imageResult.getWarnings())); + log.debug("EPUB Import - Image processing warnings: {}", + String.join(", ", imageResult.getWarnings())); } } } catch (Exception e) { @@ -282,7 +285,7 @@ public class EPUBImportService { if (language != null && !language.trim().isEmpty()) { // Store as metadata in story description if needed // For now, we'll just log it for potential future use - System.out.println("EPUB Language: " + language); + log.debug("EPUB Language: {}", language); } // Extract publisher information @@ -290,14 +293,14 @@ public class EPUBImportService { if (publishers != null && !publishers.isEmpty()) { String publisher = publishers.get(0); // Could append to description or store separately in future - System.out.println("EPUB Publisher: " + publisher); + log.debug("EPUB Publisher: {}", publisher); } // Extract publication date List dates = metadata.getDates(); if (dates != null && !dates.isEmpty()) { for (nl.siegmann.epublib.domain.Date date : dates) { - System.out.println("EPUB Date (" + date.getEvent() + "): " + date.getValue()); + log.debug("EPUB Date ({}): {}", date.getEvent(), date.getValue()); } } @@ -305,7 +308,7 @@ public class EPUBImportService { List identifiers = metadata.getIdentifiers(); if (identifiers != null && !identifiers.isEmpty()) { for (nl.siegmann.epublib.domain.Identifier identifier : identifiers) { - System.out.println("EPUB Identifier (" + identifier.getScheme() + "): " + identifier.getValue()); + log.debug("EPUB Identifier ({}): {}", identifier.getScheme(), identifier.getValue()); } } } diff --git a/backend/src/main/java/com/storycove/service/ImageService.java b/backend/src/main/java/com/storycove/service/ImageService.java index 56127e0..bf09224 100644 --- a/backend/src/main/java/com/storycove/service/ImageService.java +++ b/backend/src/main/java/com/storycove/service/ImageService.java @@ -248,14 +248,14 @@ public class ImageService { * Process HTML content and download all referenced images, replacing URLs with local paths */ public ContentImageProcessingResult processContentImages(String htmlContent, UUID storyId) { - logger.info("Processing content images for story: {}, content length: {}", storyId, + logger.debug("Processing content images for story: {}, content length: {}", storyId, htmlContent != null ? htmlContent.length() : 0); List warnings = new ArrayList<>(); List downloadedImages = new ArrayList<>(); if (htmlContent == null || htmlContent.trim().isEmpty()) { - logger.info("No content to process for story: {}", storyId); + logger.debug("No content to process for story: {}", storyId); return new ContentImageProcessingResult(htmlContent, warnings, downloadedImages); } @@ -273,18 +273,18 @@ public class ImageService { String imageUrl = matcher.group(1); imageCount++; - logger.info("Found image #{}: {} in tag: {}", imageCount, imageUrl, fullImgTag); + logger.debug("Found image #{}: {} in tag: {}", imageCount, imageUrl, fullImgTag); try { // Skip if it's already a local path or data URL if (imageUrl.startsWith("/") || imageUrl.startsWith("data:")) { - logger.info("Skipping local/data URL: {}", imageUrl); + logger.debug("Skipping local/data URL: {}", imageUrl); matcher.appendReplacement(processedContent, Matcher.quoteReplacement(fullImgTag)); continue; } externalImageCount++; - logger.info("Processing external image #{}: {}", externalImageCount, imageUrl); + logger.debug("Processing external image #{}: {}", externalImageCount, imageUrl); // Download and store the image String localPath = downloadImageFromUrl(imageUrl, storyId); @@ -292,7 +292,7 @@ public class ImageService { // Generate local URL String localUrl = getLocalImageUrl(storyId, localPath); - logger.info("Downloaded image: {} -> {}", imageUrl, localUrl); + logger.debug("Downloaded image: {} -> {}", imageUrl, localUrl); // Replace the src attribute with the local path - handle both single and double quotes String newImgTag = fullImgTag @@ -305,7 +305,7 @@ public class ImageService { newImgTag = fullImgTag.replaceAll("src\\s*=\\s*[\"']?" + Pattern.quote(imageUrl) + "[\"']?", "src=\"" + localUrl + "\""); } - logger.info("Replaced img tag: {} -> {}", fullImgTag, newImgTag); + logger.debug("Replaced img tag: {} -> {}", fullImgTag, newImgTag); matcher.appendReplacement(processedContent, Matcher.quoteReplacement(newImgTag)); } catch (Exception e) { @@ -388,7 +388,7 @@ public class ImageService { return "/api/files/images/default/" + imagePath; } String localUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath; - logger.info("Generated local image URL: {} for story: {}", localUrl, storyId); + logger.debug("Generated local image URL: {} for story: {}", localUrl, storyId); return localUrl; } @@ -437,20 +437,20 @@ public class ImageService { int foldersToDelete = 0; // Step 1: Collect all image references from all story content - logger.info("Scanning all story content for image references..."); + logger.debug("Scanning all story content for image references..."); referencedImages = collectAllImageReferences(); - logger.info("Found {} unique image references in story content", referencedImages.size()); + logger.debug("Found {} unique image references in story content", referencedImages.size()); try { // Step 2: Scan the content images directory Path contentImagesDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory()); if (!Files.exists(contentImagesDir)) { - logger.info("Content images directory does not exist: {}", contentImagesDir); + logger.debug("Content images directory does not exist: {}", contentImagesDir); return new ContentImageCleanupResult(orphanedImages, 0, 0, referencedImages.size(), errors, dryRun); } - logger.info("Scanning content images directory: {}", contentImagesDir); + logger.debug("Scanning content images directory: {}", contentImagesDir); // Walk through all story directories Files.walk(contentImagesDir, 2) @@ -465,7 +465,7 @@ public class ImageService { boolean storyExists = storyService.findByIdOptional(UUID.fromString(storyId)).isPresent(); if (!storyExists) { - logger.info("Found orphaned story directory (story deleted): {}", storyId); + logger.debug("Found orphaned story directory (story deleted): {}", storyId); // Mark entire directory for deletion try { Files.walk(storyDir) @@ -535,7 +535,7 @@ public class ImageService { // Step 3: Delete orphaned files if not dry run if (!dryRun && !orphanedImages.isEmpty()) { - logger.info("Deleting {} orphaned images...", orphanedImages.size()); + logger.debug("Deleting {} orphaned images...", orphanedImages.size()); Set directoriesToCheck = new HashSet<>(); @@ -557,7 +557,7 @@ public class ImageService { try { if (Files.exists(dir) && isDirEmpty(dir)) { Files.delete(dir); - logger.info("Deleted empty story directory: {}", dir); + logger.debug("Deleted empty story directory: {}", dir); } } catch (IOException e) { errors.add("Failed to delete empty directory " + dir + ": " + e.getMessage()); diff --git a/backend/src/main/java/com/storycove/service/LibraryService.java b/backend/src/main/java/com/storycove/service/LibraryService.java index c3e5683..22a5ba0 100644 --- a/backend/src/main/java/com/storycove/service/LibraryService.java +++ b/backend/src/main/java/com/storycove/service/LibraryService.java @@ -144,9 +144,9 @@ public class LibraryService implements ApplicationContextAware { String previousLibraryId = currentLibraryId; if (libraryId.equals(currentLibraryId) && forceReindex) { - logger.info("Forcing reindex for current library: {} ({})", library.getName(), libraryId); + logger.debug("Forcing reindex for current library: {} ({})", library.getName(), libraryId); } else { - logger.info("Switching to library: {} ({})", library.getName(), libraryId); + logger.debug("Switching to library: {} ({})", library.getName(), libraryId); } // Close current resources @@ -155,14 +155,14 @@ public class LibraryService implements ApplicationContextAware { // Set new active library (datasource routing handled by SmartRoutingDataSource) currentLibraryId = libraryId; // OpenSearch indexes are global - no per-library initialization needed - logger.info("Library switched to OpenSearch mode for library: {}", libraryId); + logger.debug("Library switched to OpenSearch mode for library: {}", libraryId); logger.info("Successfully switched to library: {}", library.getName()); // Perform complete reindex AFTER library switch is fully complete // This ensures database routing is properly established if (forceReindex || !libraryId.equals(previousLibraryId)) { - logger.info("Starting post-switch OpenSearch reindex for library: {}", libraryId); + logger.debug("Starting post-switch OpenSearch reindex for library: {}", libraryId); // Run reindex asynchronously to avoid blocking authentication response // and allow time for database routing to fully stabilize @@ -171,7 +171,7 @@ public class LibraryService implements ApplicationContextAware { try { // Give routing time to stabilize Thread.sleep(500); - logger.info("Starting async OpenSearch reindex for library: {}", finalLibraryId); + logger.debug("Starting async OpenSearch reindex for library: {}", finalLibraryId); SearchServiceAdapter searchService = applicationContext.getBean(SearchServiceAdapter.class); // Get all stories and authors for reindexing @@ -342,10 +342,10 @@ public class LibraryService implements ApplicationContextAware { library.setInitialized((Boolean) data.getOrDefault("initialized", false)); libraries.put(id, library); - logger.info("Loaded library: {} ({})", library.getName(), id); + logger.debug("Loaded library: {} ({})", library.getName(), id); } } else { - logger.info("No libraries configuration file found, will create default"); + logger.debug("No libraries configuration file found, will create default"); } } catch (IOException e) { logger.error("Failed to load libraries configuration", e); @@ -411,7 +411,7 @@ public class LibraryService implements ApplicationContextAware { String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(config); Files.writeString(Paths.get(LIBRARIES_CONFIG_PATH), json); - logger.info("Saved libraries configuration"); + logger.debug("Saved libraries configuration"); } catch (IOException e) { logger.error("Failed to save libraries configuration", e); } @@ -419,7 +419,7 @@ public class LibraryService implements ApplicationContextAware { private DataSource createDataSource(String dbName) { String url = baseDbUrl.replaceAll("/[^/]*$", "/" + dbName); - logger.info("Creating DataSource for: {}", url); + logger.debug("Creating DataSource for: {}", url); // First, ensure the database exists ensureDatabaseExists(dbName); @@ -459,7 +459,7 @@ public class LibraryService implements ApplicationContextAware { preparedStatement.setString(1, dbName); try (var resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { - logger.info("Database {} already exists", dbName); + logger.debug("Database {} already exists", dbName); return; // Database exists, nothing to do } } @@ -488,7 +488,7 @@ public class LibraryService implements ApplicationContextAware { } private void initializeNewDatabaseSchema(String dbName) { - logger.info("Initializing schema for new database: {}", dbName); + logger.debug("Initializing schema for new database: {}", dbName); // Create a temporary DataSource for the new database to initialize schema String newDbUrl = baseDbUrl.replaceAll("/[^/]*$", "/" + dbName); @@ -505,7 +505,7 @@ public class LibraryService implements ApplicationContextAware { // Use Hibernate to create the schema // This mimics what Spring Boot does during startup createSchemaUsingHibernate(tempDataSource); - logger.info("Schema initialized for database: {}", dbName); + logger.debug("Schema initialized for database: {}", dbName); } catch (Exception e) { logger.error("Failed to initialize schema for database {}: {}", dbName, e.getMessage()); @@ -520,7 +520,7 @@ public class LibraryService implements ApplicationContextAware { } try { - logger.info("Initializing resources for new library: {}", library.getName()); + logger.debug("Initializing resources for new library: {}", library.getName()); // 1. Create image directory structure initializeImageDirectories(library); @@ -528,7 +528,7 @@ public class LibraryService implements ApplicationContextAware { // 2. OpenSearch indexes are global and managed automatically // No per-library initialization needed for OpenSearch - logger.info("Successfully initialized resources for library: {}", library.getName()); + logger.debug("Successfully initialized resources for library: {}", library.getName()); } catch (Exception e) { logger.error("Failed to initialize resources for library {}: {}", libraryId, e.getMessage()); @@ -544,16 +544,16 @@ public class LibraryService implements ApplicationContextAware { if (!java.nio.file.Files.exists(libraryImagePath)) { java.nio.file.Files.createDirectories(libraryImagePath); - logger.info("Created image directory: {}", imagePath); + logger.debug("Created image directory: {}", imagePath); // Create subdirectories for different image types java.nio.file.Files.createDirectories(libraryImagePath.resolve("stories")); java.nio.file.Files.createDirectories(libraryImagePath.resolve("authors")); java.nio.file.Files.createDirectories(libraryImagePath.resolve("collections")); - logger.info("Created image subdirectories for library: {}", library.getId()); + logger.debug("Created image subdirectories for library: {}", library.getId()); } else { - logger.info("Image directory already exists: {}", imagePath); + logger.debug("Image directory already exists: {}", imagePath); } } catch (Exception e) { @@ -749,7 +749,7 @@ public class LibraryService implements ApplicationContextAware { statement.executeUpdate(sql); } - logger.info("Successfully created all database tables and constraints"); + logger.debug("Successfully created all database tables and constraints"); } catch (SQLException e) { logger.error("Failed to create database schema", e); diff --git a/backend/src/main/java/com/storycove/service/OpenSearchService.java b/backend/src/main/java/com/storycove/service/OpenSearchService.java index 589c58b..87c8f29 100644 --- a/backend/src/main/java/com/storycove/service/OpenSearchService.java +++ b/backend/src/main/java/com/storycove/service/OpenSearchService.java @@ -55,15 +55,15 @@ public class OpenSearchService { @PostConstruct public void initializeIndices() { if (!isAvailable()) { - logger.info("OpenSearch client not available - skipping index initialization"); + logger.debug("OpenSearch client not available - skipping index initialization"); return; } try { - logger.info("Initializing OpenSearch indices..."); + logger.debug("Initializing OpenSearch indices..."); createStoriesIndex(); createAuthorsIndex(); - logger.info("OpenSearch indices initialized successfully"); + logger.debug("OpenSearch indices initialized successfully"); } catch (IOException e) { logger.error("Failed to initialize OpenSearch indices", e); } @@ -80,7 +80,7 @@ public class OpenSearchService { return; } - logger.info("Creating stories index: {}", indexName); + logger.debug("Creating stories index: {}", indexName); // Create index settings programmatically IndexSettings indexSettings = IndexSettings.of(is -> is @@ -125,7 +125,7 @@ public class OpenSearchService { ); openSearchClient.indices().create(request); - logger.info("Created stories index successfully: {}", indexName); + logger.debug("Created stories index successfully: {}", indexName); } private void createAuthorsIndex() throws IOException { @@ -135,7 +135,7 @@ public class OpenSearchService { return; } - logger.info("Creating authors index: {}", indexName); + logger.debug("Creating authors index: {}", indexName); IndexSettings indexSettings = IndexSettings.of(is -> is .numberOfShards(properties.getIndices().getDefaultShards()) @@ -159,7 +159,7 @@ public class OpenSearchService { ); openSearchClient.indices().create(request); - logger.info("Created authors index successfully: {}", indexName); + logger.debug("Created authors index successfully: {}", indexName); } private boolean indexExists(String indexName) throws IOException { @@ -293,7 +293,7 @@ public class OpenSearchService { return; } - logger.info("Bulk indexing {} stories", stories.size()); + logger.debug("Bulk indexing {} stories", stories.size()); BulkRequest.Builder bulkBuilder = new BulkRequest.Builder(); @@ -319,7 +319,7 @@ public class OpenSearchService { }); } - logger.info("Successfully bulk indexed {} stories", stories.size()); + logger.debug("Successfully bulk indexed {} stories", stories.size()); } public void bulkIndexAuthors(List authors) throws IOException { @@ -333,7 +333,7 @@ public class OpenSearchService { return; } - logger.info("Bulk indexing {} authors", authors.size()); + logger.debug("Bulk indexing {} authors", authors.size()); BulkRequest.Builder bulkBuilder = new BulkRequest.Builder(); @@ -359,7 +359,7 @@ public class OpenSearchService { }); } - logger.info("Successfully bulk indexed {} authors", authors.size()); + logger.debug("Successfully bulk indexed {} authors", authors.size()); } // =============================== @@ -380,27 +380,27 @@ public class OpenSearchService { Integer minTagCount, Boolean popularOnly, Boolean hiddenGemsOnly) { try { - logger.info("OPENSEARCH SEARCH DEBUG:"); - logger.info(" Query: '{}'", query); - logger.info(" Tags: {}", tags); - logger.info(" Author: '{}'", author); - logger.info(" Series: '{}'", series); - logger.info(" SortBy: '{}'", sortBy); - logger.info(" SortOrder: '{}'", sortOrder); - logger.info(" Page: {}, Size: {}", page, size); - logger.info(" FacetBy: {}", facetBy); - logger.info(" Advanced filters: createdAfter='{}', createdBefore='{}', lastReadAfter='{}', lastReadBefore='{}'", + logger.debug("OPENSEARCH SEARCH DEBUG:"); + logger.debug(" Query: '{}'", query); + logger.debug(" Tags: {}", tags); + logger.debug(" Author: '{}'", author); + logger.debug(" Series: '{}'", series); + logger.debug(" SortBy: '{}'", sortBy); + logger.debug(" SortOrder: '{}'", sortOrder); + logger.debug(" Page: {}, Size: {}", page, size); + logger.debug(" FacetBy: {}", facetBy); + logger.debug(" Advanced filters: createdAfter='{}', createdBefore='{}', lastReadAfter='{}', lastReadBefore='{}'", createdAfter, createdBefore, lastReadAfter, lastReadBefore); - logger.info(" Boolean filters: unratedOnly={}, readingStatus='{}', hasReadingProgress={}, hasCoverImage={}", + logger.debug(" Boolean filters: unratedOnly={}, readingStatus='{}', hasReadingProgress={}, hasCoverImage={}", unratedOnly, readingStatus, hasReadingProgress, hasCoverImage); - logger.info(" Other filters: sourceDomain='{}', seriesFilter='{}', minTagCount={}, popularOnly={}, hiddenGemsOnly={}", + logger.debug(" Other filters: sourceDomain='{}', seriesFilter='{}', minTagCount={}, popularOnly={}, hiddenGemsOnly={}", sourceDomain, seriesFilter, minTagCount, popularOnly, hiddenGemsOnly); // Check index document count try { var countRequest = CountRequest.of(cr -> cr.index(getStoriesIndex())); var countResponse = openSearchClient.count(countRequest); - logger.info(" Stories index document count: {}", countResponse.count()); + logger.debug(" Stories index document count: {}", countResponse.count()); // Test a simple search without sorting to see if we get results if (countResponse.count() > 0) { @@ -410,7 +410,7 @@ public class OpenSearchService { .query(q -> q.matchAll(ma -> ma)) ); var testResponse = openSearchClient.search(testSearch, StorySearchDto.class); - logger.info(" Test search without sorting: totalHits={}, hits.size()={}", + logger.debug(" Test search without sorting: totalHits={}, hits.size()={}", testResponse.hits().total() != null ? testResponse.hits().total().value() : 0, testResponse.hits().hits().size()); } @@ -431,10 +431,10 @@ public class OpenSearchService { String trimmedQuery = query.trim(); // Handle wildcard queries if ("*".equals(trimmedQuery) || "**".equals(trimmedQuery)) { - logger.info(" Using matchAll query for wildcard: '{}'", trimmedQuery); + logger.debug(" Using matchAll query for wildcard: '{}'", trimmedQuery); boolBuilder.must(m -> m.matchAll(ma -> ma)); } else { - logger.info(" Using multiMatch query for: '{}'", trimmedQuery); + logger.debug(" Using multiMatch query for: '{}'", trimmedQuery); boolBuilder.must(m -> m .multiMatch(mm -> mm .query(trimmedQuery) @@ -444,13 +444,13 @@ public class OpenSearchService { ); } } else { - logger.info(" Using matchAll query for empty query"); + logger.debug(" Using matchAll query for empty query"); boolBuilder.must(m -> m.matchAll(ma -> ma)); } // Add filters if (tags != null && !tags.isEmpty()) { - logger.info(" Adding tags filter: {}", tags); + logger.debug(" Adding tags filter: {}", tags); boolBuilder.filter(f -> f .terms(t -> t .field("tagNames") @@ -460,7 +460,7 @@ public class OpenSearchService { } if (author != null && !author.trim().isEmpty() && !"null".equalsIgnoreCase(author.trim())) { - logger.info(" Adding author filter: '{}'", author.trim()); + logger.debug(" Adding author filter: '{}'", author.trim()); boolBuilder.filter(f -> f .term(t -> t .field("authorName") @@ -472,7 +472,7 @@ public class OpenSearchService { // Series filtering is now handled by advanced seriesFilter parameter if (minWordCount != null || maxWordCount != null) { - logger.info(" Adding word count filter: min={}, max={}", minWordCount, maxWordCount); + logger.debug(" Adding word count filter: min={}, max={}", minWordCount, maxWordCount); boolBuilder.filter(f -> f .range(r -> { var rangeBuilder = r.field("wordCount"); @@ -488,7 +488,7 @@ public class OpenSearchService { } if (minRating != null) { - logger.info(" Adding rating filter: min={}", minRating); + logger.debug(" Adding rating filter: min={}", minRating); boolBuilder.filter(f -> f .range(r -> r .field("rating") @@ -498,7 +498,7 @@ public class OpenSearchService { } if (isRead != null) { - logger.info(" Adding isRead filter: {}", isRead); + logger.debug(" Adding isRead filter: {}", isRead); boolBuilder.filter(f -> f .term(t -> t .field("isRead") @@ -508,13 +508,13 @@ public class OpenSearchService { } if (isFavorite != null) { - logger.info(" isFavorite filter requested: {} (FIELD NOT IMPLEMENTED - IGNORING)", isFavorite); + logger.debug(" isFavorite filter requested: {} (FIELD NOT IMPLEMENTED - IGNORING)", isFavorite); // isFavorite field is not implemented in Story entity or StorySearchDto, so ignore this filter } // Advanced date filters if (createdAfter != null && !createdAfter.trim().isEmpty() && !"null".equalsIgnoreCase(createdAfter.trim())) { - logger.info(" Adding createdAfter filter: '{}'", createdAfter.trim()); + logger.debug(" Adding createdAfter filter: '{}'", createdAfter.trim()); boolBuilder.filter(f -> f .range(r -> r .field("createdAt") @@ -524,7 +524,7 @@ public class OpenSearchService { } if (createdBefore != null && !createdBefore.trim().isEmpty() && !"null".equalsIgnoreCase(createdBefore.trim())) { - logger.info(" Adding createdBefore filter: '{}'", createdBefore.trim()); + logger.debug(" Adding createdBefore filter: '{}'", createdBefore.trim()); boolBuilder.filter(f -> f .range(r -> r .field("createdAt") @@ -534,7 +534,7 @@ public class OpenSearchService { } if (lastReadAfter != null && !lastReadAfter.trim().isEmpty() && !"null".equalsIgnoreCase(lastReadAfter.trim())) { - logger.info(" Adding lastReadAfter filter: '{}'", lastReadAfter.trim()); + logger.debug(" Adding lastReadAfter filter: '{}'", lastReadAfter.trim()); boolBuilder.filter(f -> f .range(r -> r .field("lastReadAt") @@ -544,7 +544,7 @@ public class OpenSearchService { } if (lastReadBefore != null && !lastReadBefore.trim().isEmpty() && !"null".equalsIgnoreCase(lastReadBefore.trim())) { - logger.info(" Adding lastReadBefore filter: '{}'", lastReadBefore.trim()); + logger.debug(" Adding lastReadBefore filter: '{}'", lastReadBefore.trim()); boolBuilder.filter(f -> f .range(r -> r .field("lastReadAt") @@ -555,7 +555,7 @@ public class OpenSearchService { // Advanced boolean filters if (unratedOnly != null && unratedOnly) { - logger.info(" Adding unratedOnly filter"); + logger.debug(" Adding unratedOnly filter"); boolBuilder.filter(f -> f .bool(b -> b .should(s -> s.term(t -> t.field("rating").value(FieldValue.of(0)))) @@ -565,7 +565,7 @@ public class OpenSearchService { } if (hasReadingProgress != null) { - logger.info(" Adding hasReadingProgress filter: {}", hasReadingProgress); + logger.debug(" Adding hasReadingProgress filter: {}", hasReadingProgress); if (hasReadingProgress) { boolBuilder.filter(f -> f .range(r -> r @@ -584,7 +584,7 @@ public class OpenSearchService { } if (hasCoverImage != null) { - logger.info(" Adding hasCoverImage filter: {}", hasCoverImage); + logger.debug(" Adding hasCoverImage filter: {}", hasCoverImage); if (hasCoverImage) { boolBuilder.filter(f -> f .exists(e -> e.field("coverPath")) @@ -599,7 +599,7 @@ public class OpenSearchService { } if (sourceDomain != null && !sourceDomain.trim().isEmpty() && !"null".equalsIgnoreCase(sourceDomain.trim())) { - logger.info(" Adding sourceDomain filter: '{}'", sourceDomain.trim()); + logger.debug(" Adding sourceDomain filter: '{}'", sourceDomain.trim()); boolBuilder.filter(f -> f .term(t -> t .field("sourceDomain") @@ -610,10 +610,10 @@ public class OpenSearchService { // Reading status filter logic if (readingStatus != null && !readingStatus.trim().isEmpty() && !"null".equalsIgnoreCase(readingStatus.trim()) && !"all".equalsIgnoreCase(readingStatus.trim())) { - logger.info(" Adding readingStatus filter: '{}'", readingStatus.trim()); + logger.debug(" Adding readingStatus filter: '{}'", readingStatus.trim()); if ("unread".equalsIgnoreCase(readingStatus.trim())) { // Simplified unread test: just check isRead = false - logger.info(" Applying simplified unread filter: isRead = false"); + logger.debug(" Applying simplified unread filter: isRead = false"); boolBuilder.filter(f -> f .term(t -> t.field("isRead").value(FieldValue.of(false))) ); @@ -635,10 +635,10 @@ public class OpenSearchService { // Series filter (separate from seriesFilter parameter which is handled above) if (seriesFilter != null && !seriesFilter.trim().isEmpty() && !"null".equalsIgnoreCase(seriesFilter.trim())) { - logger.info(" Adding advanced seriesFilter: '{}'", seriesFilter.trim()); + logger.debug(" Adding advanced seriesFilter: '{}'", seriesFilter.trim()); if ("standalone".equalsIgnoreCase(seriesFilter.trim())) { // Stories without series: seriesName field doesn't exist or is null - logger.info(" Applying standalone filter: seriesName field must not exist"); + logger.debug(" Applying standalone filter: seriesName field must not exist"); boolBuilder.filter(f -> f .bool(b -> b .mustNot(mn -> mn.exists(e -> e.field("seriesName"))) @@ -646,7 +646,7 @@ public class OpenSearchService { ); } else if ("series".equalsIgnoreCase(seriesFilter.trim())) { // Stories with series: seriesName field exists and has a value - logger.info(" Applying series filter: seriesName field must exist"); + logger.debug(" Applying series filter: seriesName field must exist"); boolBuilder.filter(f -> f .exists(e -> e.field("seriesName")) ); diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 4ec11b4..733aa3e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -139,6 +139,15 @@ @apply max-w-full h-auto mx-auto my-6 rounded-lg shadow-sm; max-height: 80vh; /* Prevent images from being too tall */ display: block; + /* Optimize for performance and prevent reloading */ + will-change: auto; + transform: translateZ(0); /* Force hardware acceleration */ + backface-visibility: hidden; + image-rendering: optimizeQuality; + /* Prevent layout shifts that might trigger reloads */ + box-sizing: border-box; + /* Ensure stable dimensions */ + min-height: 1px; } .reading-content img[align="left"] { diff --git a/frontend/src/app/stories/[id]/page.tsx b/frontend/src/app/stories/[id]/page.tsx index f35e71d..e24ced0 100644 --- a/frontend/src/app/stories/[id]/page.tsx +++ b/frontend/src/app/stories/[id]/page.tsx @@ -11,6 +11,7 @@ import StoryRating from '../../../components/stories/StoryRating'; import TagDisplay from '../../../components/tags/TagDisplay'; import TableOfContents from '../../../components/stories/TableOfContents'; import { sanitizeHtml, preloadSanitizationConfig } from '../../../lib/sanitization'; +import { debug } from '../../../lib/debug'; // Memoized content component that only re-renders when content changes const StoryContent = memo(({ @@ -20,13 +21,50 @@ const StoryContent = memo(({ content: string; contentRef: React.RefObject; }) => { - console.log('🔄 StoryContent component rendering with content length:', content.length); + const renderTime = Date.now(); + debug.log('🔄 StoryContent component rendering at', renderTime, 'with content length:', content.length, 'hash:', content.slice(0, 50) + '...'); + + // Add observer to track image loading events + useEffect(() => { + if (!contentRef.current) return; + + const images = contentRef.current.querySelectorAll('img'); + debug.log('📸 Found', images.length, 'images in content'); + + const handleImageLoad = (e: Event) => { + const img = e.target as HTMLImageElement; + debug.log('🖼️ Image loaded:', img.src); + }; + + const handleImageError = (e: Event) => { + const img = e.target as HTMLImageElement; + debug.log('❌ Image error:', img.src); + }; + + images.forEach(img => { + img.addEventListener('load', handleImageLoad); + img.addEventListener('error', handleImageError); + debug.log('👀 Monitoring image:', img.src); + }); + + return () => { + images.forEach(img => { + img.removeEventListener('load', handleImageLoad); + img.removeEventListener('error', handleImageError); + }); + }; + }, [content]); return (
); }); @@ -112,14 +150,14 @@ export default function StoryReadingPage() { // Debounced function to save reading position const saveReadingPosition = useCallback(async (position: number) => { if (!story || position === story.readingPosition) { - console.log('Skipping save - no story or position unchanged:', { story: !!story, position, current: story?.readingPosition }); + debug.log('Skipping save - no story or position unchanged:', { story: !!story, position, current: story?.readingPosition }); return; } - console.log('Saving reading position:', position, 'for story:', story.id); + debug.log('Saving reading position:', position, 'for story:', story.id); try { const updatedStory = await storyApi.updateReadingProgress(story.id, position); - console.log('Reading position saved successfully, updated story:', updatedStory.readingPosition); + debug.log('Reading position saved successfully, updated story:', updatedStory.readingPosition); setStory(prev => prev ? { ...prev, readingPosition: position, lastReadAt: updatedStory.lastReadAt } : null); } catch (error) { console.error('Failed to save reading position:', error); @@ -200,12 +238,12 @@ export default function StoryReadingPage() { if (story && sanitizedContent && !hasScrolledToPosition) { // Use a small delay to ensure content is rendered const timeout = setTimeout(() => { - console.log('Initializing reading position tracking, saved position:', story.readingPosition); + debug.log('Initializing reading position tracking, saved position:', story.readingPosition); // Check if there's a hash in the URL (for TOC navigation) const hash = window.location.hash.substring(1); if (hash && hash.startsWith('heading-')) { - console.log('Auto-scrolling to heading from URL hash:', hash); + debug.log('Auto-scrolling to heading from URL hash:', hash); const element = document.getElementById(hash); if (element) { element.scrollIntoView({ @@ -219,13 +257,13 @@ export default function StoryReadingPage() { // Otherwise, use saved reading position if (story.readingPosition && story.readingPosition > 0) { - console.log('Auto-scrolling to saved position:', story.readingPosition); + debug.log('Auto-scrolling to saved position:', story.readingPosition); const initialPercentage = calculateReadingPercentage(story.readingPosition); setReadingPercentage(initialPercentage); scrollToCharacterPosition(story.readingPosition); } else { // Even if there's no saved position, mark as ready for tracking - console.log('No saved position, starting fresh tracking'); + debug.log('No saved position, starting fresh tracking'); setReadingPercentage(0); setHasScrolledToPosition(true); } @@ -238,8 +276,14 @@ export default function StoryReadingPage() { // Track reading progress and save position useEffect(() => { let ticking = false; + let scrollEventCount = 0; const handleScroll = () => { + scrollEventCount++; + if (scrollEventCount % 10 === 0) { + debug.log('📜 Scroll event #', scrollEventCount, 'at', Date.now()); + } + if (!ticking) { requestAnimationFrame(() => { const article = document.querySelector('[data-reading-content]') as HTMLElement; @@ -278,7 +322,7 @@ export default function StoryReadingPage() { // Trigger end detection if user is near bottom AND (has high progress OR story content is fully visible) if (nearBottom && (highProgress || storyContentFullyVisible) && !hasReachedEnd && hasScrolledToPosition) { - console.log('End of story detected:', { nearBottom, highProgress, storyContentFullyVisible, distanceFromBottom, progress }); + debug.log('End of story detected:', { nearBottom, highProgress, storyContentFullyVisible, distanceFromBottom, progress }); setHasReachedEnd(true); setShowEndOfStoryPopup(true); } @@ -287,11 +331,11 @@ export default function StoryReadingPage() { if (hasScrolledToPosition) { // Only save after initial auto-scroll const characterPosition = getCharacterPositionFromScroll(); const percentage = calculateReadingPercentage(characterPosition); - console.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage); + debug.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage); setReadingPercentage(percentage); debouncedSavePosition(characterPosition); } else { - console.log('Scroll detected but not ready for tracking yet'); + debug.log('Scroll detected but not ready for tracking yet'); } } ticking = false; diff --git a/frontend/src/components/stories/PortableTextEditorNew.tsx b/frontend/src/components/stories/PortableTextEditorNew.tsx index 99de39c..b77197c 100644 --- a/frontend/src/components/stories/PortableTextEditorNew.tsx +++ b/frontend/src/components/stories/PortableTextEditorNew.tsx @@ -16,6 +16,7 @@ import { PortableText } from '@portabletext/react'; import Button from '../ui/Button'; import { sanitizeHtmlSync } from '../../lib/sanitization'; import { editorSchema } from '../../lib/portabletext/editorSchema'; +import { debug } from '../../lib/debug'; interface PortableTextEditorProps { value: string; // HTML value for compatibility - will be converted @@ -394,13 +395,13 @@ function EditorContent({ // Sync HTML value with prop changes useEffect(() => { - console.log('🔄 Editor value changed:', { valueLength: value?.length, valuePreview: value?.substring(0, 100) }); + debug.log('🔄 Editor value changed:', { valueLength: value?.length, valuePreview: value?.substring(0, 100) }); setPortableTextValue(htmlToPortableTextBlocks(value)); }, [value]); // Debug: log when portableTextValue changes useEffect(() => { - console.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue }); + debug.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue }); }, [portableTextValue]); // Add a ref to the editor container for direct paste handling @@ -409,13 +410,13 @@ function EditorContent({ // Global paste event listener to catch ALL paste events useEffect(() => { const handleGlobalPaste = (event: ClipboardEvent) => { - console.log('🌍 Global paste event captured'); + debug.log('🌍 Global paste event captured'); // Check if the paste is happening within our editor const target = event.target as Element; const isInEditor = editorContainerRef.current?.contains(target); - console.log('📋 Paste details:', { + debug.log('📋 Paste details:', { isInEditor, targetTag: target?.tagName, targetClasses: target?.className, @@ -426,7 +427,7 @@ function EditorContent({ const htmlData = event.clipboardData.getData('text/html'); const textData = event.clipboardData.getData('text/plain'); - console.log('📋 Clipboard contents:', { + debug.log('📋 Clipboard contents:', { htmlLength: htmlData.length, textLength: textData.length, hasImages: htmlData.includes(' ({ + debug.log('📋 Converted blocks:', pastedBlocks.map(block => ({ type: block._type, key: block._key, ...(block._type === 'image' ? { src: (block as any).src, alt: (block as any).alt } : {}), @@ -457,7 +458,7 @@ function EditorContent({ const updatedBlocks = [...prev, ...pastedBlocks]; const html = portableTextToHtml(updatedBlocks); onChange(html); - console.log('📋 Added structured blocks maintaining order:', { pastedCount: pastedBlocks.length, totalBlocks: updatedBlocks.length }); + debug.log('📋 Added structured blocks maintaining order:', { pastedCount: pastedBlocks.length, totalBlocks: updatedBlocks.length }); return updatedBlocks; }); }, 10); @@ -476,7 +477,7 @@ function EditorContent({ // Handle paste events directly on the editor container (backup approach) const handleContainerPaste = useCallback((_event: React.ClipboardEvent) => { - console.log('📦 Container paste handler triggered'); + debug.log('📦 Container paste handler triggered'); // This might not be reached if global handler prevents default }, []); @@ -526,11 +527,11 @@ function EditorContent({ const renderBlock: RenderBlockFunction = useCallback((props) => { const { schemaType, value, children } = props; - console.log('🎨 Rendering block:', { schemaType: schemaType.name, valueType: value?._type, value }); + debug.log('🎨 Rendering block:', { schemaType: schemaType.name, valueType: value?._type, value }); // Handle image blocks if (schemaType.name === 'image' && isImageBlock(value)) { - console.log('🖼️ Rendering image block:', value); + debug.log('🖼️ Rendering image block:', value); return (
@@ -654,7 +655,7 @@ export default function PortableTextEditorNew({ storyId, enableImageProcessing = false }: PortableTextEditorProps) { - console.log('🎯 Portable Text Editor loaded!', { + debug.log('🎯 Portable Text Editor loaded!', { valueLength: value?.length, enableImageProcessing, hasStoryId: !!storyId diff --git a/frontend/src/lib/debug.ts b/frontend/src/lib/debug.ts new file mode 100644 index 0000000..474a9ce --- /dev/null +++ b/frontend/src/lib/debug.ts @@ -0,0 +1,90 @@ +/** + * Debug logging utility + * Allows conditional logging based on environment or debug flags + */ + +// Check if we're in development mode or debug is explicitly enabled +const isDebugEnabled = (): boolean => { + if (typeof window === 'undefined') { + // Server-side: check NODE_ENV + return process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true'; + } + + // Client-side: check localStorage flag or development mode + try { + return ( + process.env.NODE_ENV === 'development' || + localStorage.getItem('debug') === 'true' || + window.location.search.includes('debug=true') + ); + } catch { + return process.env.NODE_ENV === 'development'; + } +}; + +/** + * Debug logger that only outputs in development or when debug is enabled + */ +export const debug = { + log: (...args: any[]) => { + if (isDebugEnabled()) { + console.log('[DEBUG]', ...args); + } + }, + + warn: (...args: any[]) => { + if (isDebugEnabled()) { + console.warn('[DEBUG]', ...args); + } + }, + + error: (...args: any[]) => { + if (isDebugEnabled()) { + console.error('[DEBUG]', ...args); + } + }, + + group: (label: string) => { + if (isDebugEnabled()) { + console.group(`[DEBUG] ${label}`); + } + }, + + groupEnd: () => { + if (isDebugEnabled()) { + console.groupEnd(); + } + }, + + time: (label: string) => { + if (isDebugEnabled()) { + console.time(`[DEBUG] ${label}`); + } + }, + + timeEnd: (label: string) => { + if (isDebugEnabled()) { + console.timeEnd(`[DEBUG] ${label}`); + } + } +}; + +/** + * Enable debug mode (persists in localStorage) + */ +export const enableDebug = () => { + if (typeof window !== 'undefined') { + localStorage.setItem('debug', 'true'); + console.log('Debug mode enabled. Reload page to see debug output.'); + } +}; + +/** + * Disable debug mode + */ +export const disableDebug = () => { + if (typeof window !== 'undefined') { + localStorage.removeItem('debug'); + console.log('Debug mode disabled. Reload page to hide debug output.'); + } +}; \ No newline at end of file diff --git a/frontend/src/lib/sanitization.ts b/frontend/src/lib/sanitization.ts index 36c4386..06cd102 100644 --- a/frontend/src/lib/sanitization.ts +++ b/frontend/src/lib/sanitization.ts @@ -1,5 +1,6 @@ import DOMPurify from 'dompurify'; import { configApi } from './api'; +import { debug } from './debug'; interface SanitizationConfig { allowedTags: string[]; @@ -28,7 +29,7 @@ function filterCssProperties(styleValue: string, allowedProperties: string[]): s const isAllowed = allowedProperties.includes(property); if (!isAllowed) { - console.log(`CSS property "${property}" was filtered out (not in allowed list)`); + debug.log(`CSS property "${property}" was filtered out (not in allowed list)`); } return isAllowed; @@ -37,9 +38,9 @@ function filterCssProperties(styleValue: string, allowedProperties: string[]): s const result = filteredDeclarations.join('; '); if (declarations.length !== filteredDeclarations.length) { - console.log(`CSS filtering: ${declarations.length} -> ${filteredDeclarations.length} properties`); - console.log('Original:', styleValue); - console.log('Filtered:', result); + debug.log(`CSS filtering: ${declarations.length} -> ${filteredDeclarations.length} properties`); + debug.log('Original:', styleValue); + debug.log('Filtered:', result); } return result; @@ -219,7 +220,7 @@ export function sanitizeHtmlSync(html: string): string { // If we don't have cached config but there's an ongoing request, wait for it if (configPromise) { - console.log('Sanitization config loading in progress, using fallback for now'); + debug.log('Sanitization config loading in progress, using fallback for now'); } else { // No config and no ongoing request - try to load it for next time console.warn('No cached sanitization config available, triggering load for future use'); @@ -229,7 +230,7 @@ export function sanitizeHtmlSync(html: string): string { } // Use comprehensive fallback configuration that preserves formatting - console.log('Using fallback sanitization configuration with formatting support'); + debug.log('Using fallback sanitization configuration with formatting support'); const fallbackAllowedCssProperties = [ 'color', 'font-size', 'font-weight', 'font-style', 'text-align', 'text-decoration', 'margin',