bugfixes, and logging cleanup
This commit is contained in:
@@ -19,7 +19,7 @@ JWT_SECRET=REPLACE_WITH_SECURE_JWT_SECRET_MINIMUM_32_CHARS
|
|||||||
APP_PASSWORD=REPLACE_WITH_SECURE_APP_PASSWORD
|
APP_PASSWORD=REPLACE_WITH_SECURE_APP_PASSWORD
|
||||||
|
|
||||||
# OpenSearch Configuration
|
# OpenSearch Configuration
|
||||||
OPENSEARCH_PASSWORD=REPLACE_WITH_SECURE_OPENSEARCH_PASSWORD
|
#OPENSEARCH_PASSWORD=REPLACE_WITH_SECURE_OPENSEARCH_PASSWORD
|
||||||
SEARCH_ENGINE=opensearch
|
SEARCH_ENGINE=opensearch
|
||||||
|
|
||||||
# Image Storage
|
# Image Storage
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.storycove.controller;
|
|||||||
|
|
||||||
import com.storycove.service.ImageService;
|
import com.storycove.service.ImageService;
|
||||||
import com.storycove.service.LibraryService;
|
import com.storycove.service.LibraryService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -21,6 +23,7 @@ import java.util.Map;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/files")
|
@RequestMapping("/api/files")
|
||||||
public class FileController {
|
public class FileController {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(FileController.class);
|
||||||
|
|
||||||
private final ImageService imageService;
|
private final ImageService imageService;
|
||||||
private final LibraryService libraryService;
|
private final LibraryService libraryService;
|
||||||
@@ -32,7 +35,7 @@ public class FileController {
|
|||||||
|
|
||||||
private String getCurrentLibraryId() {
|
private String getCurrentLibraryId() {
|
||||||
String libraryId = libraryService.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";
|
return libraryId != null ? libraryId : "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ public class FileController {
|
|||||||
String imageUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath;
|
String imageUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath;
|
||||||
response.put("url", imageUrl);
|
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);
|
return ResponseEntity.ok(response);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import nl.siegmann.epublib.epub.EpubReader;
|
|||||||
|
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -30,6 +32,7 @@ import java.util.Optional;
|
|||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
public class EPUBImportService {
|
public class EPUBImportService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(EPUBImportService.class);
|
||||||
|
|
||||||
private final StoryService storyService;
|
private final StoryService storyService;
|
||||||
private final AuthorService authorService;
|
private final AuthorService authorService;
|
||||||
@@ -87,11 +90,11 @@ public class EPUBImportService {
|
|||||||
savedStory = storyService.update(savedStory.getId(), savedStory);
|
savedStory = storyService.update(savedStory.getId(), savedStory);
|
||||||
|
|
||||||
// Log the image processing results
|
// Log the image processing results
|
||||||
System.out.println("EPUB Import - Image processing completed for story " + savedStory.getId() +
|
log.debug("EPUB Import - Image processing completed for story {}. Downloaded {} images.",
|
||||||
". Downloaded " + imageResult.getDownloadedImages().size() + " images.");
|
savedStory.getId(), imageResult.getDownloadedImages().size());
|
||||||
|
|
||||||
if (imageResult.hasWarnings()) {
|
if (imageResult.hasWarnings()) {
|
||||||
System.out.println("EPUB Import - Image processing warnings: " +
|
log.debug("EPUB Import - Image processing warnings: {}",
|
||||||
String.join(", ", imageResult.getWarnings()));
|
String.join(", ", imageResult.getWarnings()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,7 +285,7 @@ public class EPUBImportService {
|
|||||||
if (language != null && !language.trim().isEmpty()) {
|
if (language != null && !language.trim().isEmpty()) {
|
||||||
// Store as metadata in story description if needed
|
// Store as metadata in story description if needed
|
||||||
// For now, we'll just log it for potential future use
|
// 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
|
// Extract publisher information
|
||||||
@@ -290,14 +293,14 @@ public class EPUBImportService {
|
|||||||
if (publishers != null && !publishers.isEmpty()) {
|
if (publishers != null && !publishers.isEmpty()) {
|
||||||
String publisher = publishers.get(0);
|
String publisher = publishers.get(0);
|
||||||
// Could append to description or store separately in future
|
// Could append to description or store separately in future
|
||||||
System.out.println("EPUB Publisher: " + publisher);
|
log.debug("EPUB Publisher: {}", publisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract publication date
|
// Extract publication date
|
||||||
List<nl.siegmann.epublib.domain.Date> dates = metadata.getDates();
|
List<nl.siegmann.epublib.domain.Date> dates = metadata.getDates();
|
||||||
if (dates != null && !dates.isEmpty()) {
|
if (dates != null && !dates.isEmpty()) {
|
||||||
for (nl.siegmann.epublib.domain.Date date : dates) {
|
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<nl.siegmann.epublib.domain.Identifier> identifiers = metadata.getIdentifiers();
|
List<nl.siegmann.epublib.domain.Identifier> identifiers = metadata.getIdentifiers();
|
||||||
if (identifiers != null && !identifiers.isEmpty()) {
|
if (identifiers != null && !identifiers.isEmpty()) {
|
||||||
for (nl.siegmann.epublib.domain.Identifier identifier : identifiers) {
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,14 +248,14 @@ public class ImageService {
|
|||||||
* Process HTML content and download all referenced images, replacing URLs with local paths
|
* Process HTML content and download all referenced images, replacing URLs with local paths
|
||||||
*/
|
*/
|
||||||
public ContentImageProcessingResult processContentImages(String htmlContent, UUID storyId) {
|
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);
|
htmlContent != null ? htmlContent.length() : 0);
|
||||||
|
|
||||||
List<String> warnings = new ArrayList<>();
|
List<String> warnings = new ArrayList<>();
|
||||||
List<String> downloadedImages = new ArrayList<>();
|
List<String> downloadedImages = new ArrayList<>();
|
||||||
|
|
||||||
if (htmlContent == null || htmlContent.trim().isEmpty()) {
|
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);
|
return new ContentImageProcessingResult(htmlContent, warnings, downloadedImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,18 +273,18 @@ public class ImageService {
|
|||||||
String imageUrl = matcher.group(1);
|
String imageUrl = matcher.group(1);
|
||||||
imageCount++;
|
imageCount++;
|
||||||
|
|
||||||
logger.info("Found image #{}: {} in tag: {}", imageCount, imageUrl, fullImgTag);
|
logger.debug("Found image #{}: {} in tag: {}", imageCount, imageUrl, fullImgTag);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Skip if it's already a local path or data URL
|
// Skip if it's already a local path or data URL
|
||||||
if (imageUrl.startsWith("/") || imageUrl.startsWith("data:")) {
|
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));
|
matcher.appendReplacement(processedContent, Matcher.quoteReplacement(fullImgTag));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
externalImageCount++;
|
externalImageCount++;
|
||||||
logger.info("Processing external image #{}: {}", externalImageCount, imageUrl);
|
logger.debug("Processing external image #{}: {}", externalImageCount, imageUrl);
|
||||||
|
|
||||||
// Download and store the image
|
// Download and store the image
|
||||||
String localPath = downloadImageFromUrl(imageUrl, storyId);
|
String localPath = downloadImageFromUrl(imageUrl, storyId);
|
||||||
@@ -292,7 +292,7 @@ public class ImageService {
|
|||||||
|
|
||||||
// Generate local URL
|
// Generate local URL
|
||||||
String localUrl = getLocalImageUrl(storyId, localPath);
|
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
|
// Replace the src attribute with the local path - handle both single and double quotes
|
||||||
String newImgTag = fullImgTag
|
String newImgTag = fullImgTag
|
||||||
@@ -305,7 +305,7 @@ public class ImageService {
|
|||||||
newImgTag = fullImgTag.replaceAll("src\\s*=\\s*[\"']?" + Pattern.quote(imageUrl) + "[\"']?", "src=\"" + localUrl + "\"");
|
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));
|
matcher.appendReplacement(processedContent, Matcher.quoteReplacement(newImgTag));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -388,7 +388,7 @@ public class ImageService {
|
|||||||
return "/api/files/images/default/" + imagePath;
|
return "/api/files/images/default/" + imagePath;
|
||||||
}
|
}
|
||||||
String localUrl = "/api/files/images/" + currentLibraryId + "/" + 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;
|
return localUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,20 +437,20 @@ public class ImageService {
|
|||||||
int foldersToDelete = 0;
|
int foldersToDelete = 0;
|
||||||
|
|
||||||
// Step 1: Collect all image references from all story content
|
// 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();
|
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 {
|
try {
|
||||||
// Step 2: Scan the content images directory
|
// Step 2: Scan the content images directory
|
||||||
Path contentImagesDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory());
|
Path contentImagesDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory());
|
||||||
|
|
||||||
if (!Files.exists(contentImagesDir)) {
|
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);
|
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
|
// Walk through all story directories
|
||||||
Files.walk(contentImagesDir, 2)
|
Files.walk(contentImagesDir, 2)
|
||||||
@@ -465,7 +465,7 @@ public class ImageService {
|
|||||||
boolean storyExists = storyService.findByIdOptional(UUID.fromString(storyId)).isPresent();
|
boolean storyExists = storyService.findByIdOptional(UUID.fromString(storyId)).isPresent();
|
||||||
|
|
||||||
if (!storyExists) {
|
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
|
// Mark entire directory for deletion
|
||||||
try {
|
try {
|
||||||
Files.walk(storyDir)
|
Files.walk(storyDir)
|
||||||
@@ -535,7 +535,7 @@ public class ImageService {
|
|||||||
|
|
||||||
// Step 3: Delete orphaned files if not dry run
|
// Step 3: Delete orphaned files if not dry run
|
||||||
if (!dryRun && !orphanedImages.isEmpty()) {
|
if (!dryRun && !orphanedImages.isEmpty()) {
|
||||||
logger.info("Deleting {} orphaned images...", orphanedImages.size());
|
logger.debug("Deleting {} orphaned images...", orphanedImages.size());
|
||||||
|
|
||||||
Set<Path> directoriesToCheck = new HashSet<>();
|
Set<Path> directoriesToCheck = new HashSet<>();
|
||||||
|
|
||||||
@@ -557,7 +557,7 @@ public class ImageService {
|
|||||||
try {
|
try {
|
||||||
if (Files.exists(dir) && isDirEmpty(dir)) {
|
if (Files.exists(dir) && isDirEmpty(dir)) {
|
||||||
Files.delete(dir);
|
Files.delete(dir);
|
||||||
logger.info("Deleted empty story directory: {}", dir);
|
logger.debug("Deleted empty story directory: {}", dir);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
errors.add("Failed to delete empty directory " + dir + ": " + e.getMessage());
|
errors.add("Failed to delete empty directory " + dir + ": " + e.getMessage());
|
||||||
|
|||||||
@@ -144,9 +144,9 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
String previousLibraryId = currentLibraryId;
|
String previousLibraryId = currentLibraryId;
|
||||||
|
|
||||||
if (libraryId.equals(currentLibraryId) && forceReindex) {
|
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 {
|
} else {
|
||||||
logger.info("Switching to library: {} ({})", library.getName(), libraryId);
|
logger.debug("Switching to library: {} ({})", library.getName(), libraryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close current resources
|
// Close current resources
|
||||||
@@ -155,14 +155,14 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
// Set new active library (datasource routing handled by SmartRoutingDataSource)
|
// Set new active library (datasource routing handled by SmartRoutingDataSource)
|
||||||
currentLibraryId = libraryId;
|
currentLibraryId = libraryId;
|
||||||
// OpenSearch indexes are global - no per-library initialization needed
|
// 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());
|
logger.info("Successfully switched to library: {}", library.getName());
|
||||||
|
|
||||||
// Perform complete reindex AFTER library switch is fully complete
|
// Perform complete reindex AFTER library switch is fully complete
|
||||||
// This ensures database routing is properly established
|
// This ensures database routing is properly established
|
||||||
if (forceReindex || !libraryId.equals(previousLibraryId)) {
|
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
|
// Run reindex asynchronously to avoid blocking authentication response
|
||||||
// and allow time for database routing to fully stabilize
|
// and allow time for database routing to fully stabilize
|
||||||
@@ -171,7 +171,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
try {
|
try {
|
||||||
// Give routing time to stabilize
|
// Give routing time to stabilize
|
||||||
Thread.sleep(500);
|
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);
|
SearchServiceAdapter searchService = applicationContext.getBean(SearchServiceAdapter.class);
|
||||||
// Get all stories and authors for reindexing
|
// Get all stories and authors for reindexing
|
||||||
@@ -342,10 +342,10 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
library.setInitialized((Boolean) data.getOrDefault("initialized", false));
|
library.setInitialized((Boolean) data.getOrDefault("initialized", false));
|
||||||
|
|
||||||
libraries.put(id, library);
|
libraries.put(id, library);
|
||||||
logger.info("Loaded library: {} ({})", library.getName(), id);
|
logger.debug("Loaded library: {} ({})", library.getName(), id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info("No libraries configuration file found, will create default");
|
logger.debug("No libraries configuration file found, will create default");
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Failed to load libraries configuration", e);
|
logger.error("Failed to load libraries configuration", e);
|
||||||
@@ -411,7 +411,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(config);
|
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(config);
|
||||||
Files.writeString(Paths.get(LIBRARIES_CONFIG_PATH), json);
|
Files.writeString(Paths.get(LIBRARIES_CONFIG_PATH), json);
|
||||||
|
|
||||||
logger.info("Saved libraries configuration");
|
logger.debug("Saved libraries configuration");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Failed to save libraries configuration", e);
|
logger.error("Failed to save libraries configuration", e);
|
||||||
}
|
}
|
||||||
@@ -419,7 +419,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
|
|
||||||
private DataSource createDataSource(String dbName) {
|
private DataSource createDataSource(String dbName) {
|
||||||
String url = baseDbUrl.replaceAll("/[^/]*$", "/" + dbName);
|
String url = baseDbUrl.replaceAll("/[^/]*$", "/" + dbName);
|
||||||
logger.info("Creating DataSource for: {}", url);
|
logger.debug("Creating DataSource for: {}", url);
|
||||||
|
|
||||||
// First, ensure the database exists
|
// First, ensure the database exists
|
||||||
ensureDatabaseExists(dbName);
|
ensureDatabaseExists(dbName);
|
||||||
@@ -459,7 +459,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
preparedStatement.setString(1, dbName);
|
preparedStatement.setString(1, dbName);
|
||||||
try (var resultSet = preparedStatement.executeQuery()) {
|
try (var resultSet = preparedStatement.executeQuery()) {
|
||||||
if (resultSet.next()) {
|
if (resultSet.next()) {
|
||||||
logger.info("Database {} already exists", dbName);
|
logger.debug("Database {} already exists", dbName);
|
||||||
return; // Database exists, nothing to do
|
return; // Database exists, nothing to do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,7 +488,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeNewDatabaseSchema(String dbName) {
|
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
|
// Create a temporary DataSource for the new database to initialize schema
|
||||||
String newDbUrl = baseDbUrl.replaceAll("/[^/]*$", "/" + dbName);
|
String newDbUrl = baseDbUrl.replaceAll("/[^/]*$", "/" + dbName);
|
||||||
@@ -505,7 +505,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
// Use Hibernate to create the schema
|
// Use Hibernate to create the schema
|
||||||
// This mimics what Spring Boot does during startup
|
// This mimics what Spring Boot does during startup
|
||||||
createSchemaUsingHibernate(tempDataSource);
|
createSchemaUsingHibernate(tempDataSource);
|
||||||
logger.info("Schema initialized for database: {}", dbName);
|
logger.debug("Schema initialized for database: {}", dbName);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to initialize schema for database {}: {}", dbName, e.getMessage());
|
logger.error("Failed to initialize schema for database {}: {}", dbName, e.getMessage());
|
||||||
@@ -520,7 +520,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info("Initializing resources for new library: {}", library.getName());
|
logger.debug("Initializing resources for new library: {}", library.getName());
|
||||||
|
|
||||||
// 1. Create image directory structure
|
// 1. Create image directory structure
|
||||||
initializeImageDirectories(library);
|
initializeImageDirectories(library);
|
||||||
@@ -528,7 +528,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
// 2. OpenSearch indexes are global and managed automatically
|
// 2. OpenSearch indexes are global and managed automatically
|
||||||
// No per-library initialization needed for OpenSearch
|
// 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) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to initialize resources for library {}: {}", libraryId, e.getMessage());
|
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)) {
|
if (!java.nio.file.Files.exists(libraryImagePath)) {
|
||||||
java.nio.file.Files.createDirectories(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
|
// Create subdirectories for different image types
|
||||||
java.nio.file.Files.createDirectories(libraryImagePath.resolve("stories"));
|
java.nio.file.Files.createDirectories(libraryImagePath.resolve("stories"));
|
||||||
java.nio.file.Files.createDirectories(libraryImagePath.resolve("authors"));
|
java.nio.file.Files.createDirectories(libraryImagePath.resolve("authors"));
|
||||||
java.nio.file.Files.createDirectories(libraryImagePath.resolve("collections"));
|
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 {
|
} else {
|
||||||
logger.info("Image directory already exists: {}", imagePath);
|
logger.debug("Image directory already exists: {}", imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -749,7 +749,7 @@ public class LibraryService implements ApplicationContextAware {
|
|||||||
statement.executeUpdate(sql);
|
statement.executeUpdate(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Successfully created all database tables and constraints");
|
logger.debug("Successfully created all database tables and constraints");
|
||||||
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
logger.error("Failed to create database schema", e);
|
logger.error("Failed to create database schema", e);
|
||||||
|
|||||||
@@ -55,15 +55,15 @@ public class OpenSearchService {
|
|||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initializeIndices() {
|
public void initializeIndices() {
|
||||||
if (!isAvailable()) {
|
if (!isAvailable()) {
|
||||||
logger.info("OpenSearch client not available - skipping index initialization");
|
logger.debug("OpenSearch client not available - skipping index initialization");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info("Initializing OpenSearch indices...");
|
logger.debug("Initializing OpenSearch indices...");
|
||||||
createStoriesIndex();
|
createStoriesIndex();
|
||||||
createAuthorsIndex();
|
createAuthorsIndex();
|
||||||
logger.info("OpenSearch indices initialized successfully");
|
logger.debug("OpenSearch indices initialized successfully");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Failed to initialize OpenSearch indices", e);
|
logger.error("Failed to initialize OpenSearch indices", e);
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ public class OpenSearchService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Creating stories index: {}", indexName);
|
logger.debug("Creating stories index: {}", indexName);
|
||||||
|
|
||||||
// Create index settings programmatically
|
// Create index settings programmatically
|
||||||
IndexSettings indexSettings = IndexSettings.of(is -> is
|
IndexSettings indexSettings = IndexSettings.of(is -> is
|
||||||
@@ -125,7 +125,7 @@ public class OpenSearchService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
openSearchClient.indices().create(request);
|
openSearchClient.indices().create(request);
|
||||||
logger.info("Created stories index successfully: {}", indexName);
|
logger.debug("Created stories index successfully: {}", indexName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createAuthorsIndex() throws IOException {
|
private void createAuthorsIndex() throws IOException {
|
||||||
@@ -135,7 +135,7 @@ public class OpenSearchService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Creating authors index: {}", indexName);
|
logger.debug("Creating authors index: {}", indexName);
|
||||||
|
|
||||||
IndexSettings indexSettings = IndexSettings.of(is -> is
|
IndexSettings indexSettings = IndexSettings.of(is -> is
|
||||||
.numberOfShards(properties.getIndices().getDefaultShards())
|
.numberOfShards(properties.getIndices().getDefaultShards())
|
||||||
@@ -159,7 +159,7 @@ public class OpenSearchService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
openSearchClient.indices().create(request);
|
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 {
|
private boolean indexExists(String indexName) throws IOException {
|
||||||
@@ -293,7 +293,7 @@ public class OpenSearchService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Bulk indexing {} stories", stories.size());
|
logger.debug("Bulk indexing {} stories", stories.size());
|
||||||
|
|
||||||
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();
|
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<Author> authors) throws IOException {
|
public void bulkIndexAuthors(List<Author> authors) throws IOException {
|
||||||
@@ -333,7 +333,7 @@ public class OpenSearchService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Bulk indexing {} authors", authors.size());
|
logger.debug("Bulk indexing {} authors", authors.size());
|
||||||
|
|
||||||
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();
|
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,
|
Integer minTagCount, Boolean popularOnly,
|
||||||
Boolean hiddenGemsOnly) {
|
Boolean hiddenGemsOnly) {
|
||||||
try {
|
try {
|
||||||
logger.info("OPENSEARCH SEARCH DEBUG:");
|
logger.debug("OPENSEARCH SEARCH DEBUG:");
|
||||||
logger.info(" Query: '{}'", query);
|
logger.debug(" Query: '{}'", query);
|
||||||
logger.info(" Tags: {}", tags);
|
logger.debug(" Tags: {}", tags);
|
||||||
logger.info(" Author: '{}'", author);
|
logger.debug(" Author: '{}'", author);
|
||||||
logger.info(" Series: '{}'", series);
|
logger.debug(" Series: '{}'", series);
|
||||||
logger.info(" SortBy: '{}'", sortBy);
|
logger.debug(" SortBy: '{}'", sortBy);
|
||||||
logger.info(" SortOrder: '{}'", sortOrder);
|
logger.debug(" SortOrder: '{}'", sortOrder);
|
||||||
logger.info(" Page: {}, Size: {}", page, size);
|
logger.debug(" Page: {}, Size: {}", page, size);
|
||||||
logger.info(" FacetBy: {}", facetBy);
|
logger.debug(" FacetBy: {}", facetBy);
|
||||||
logger.info(" Advanced filters: createdAfter='{}', createdBefore='{}', lastReadAfter='{}', lastReadBefore='{}'",
|
logger.debug(" Advanced filters: createdAfter='{}', createdBefore='{}', lastReadAfter='{}', lastReadBefore='{}'",
|
||||||
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);
|
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);
|
sourceDomain, seriesFilter, minTagCount, popularOnly, hiddenGemsOnly);
|
||||||
|
|
||||||
// Check index document count
|
// Check index document count
|
||||||
try {
|
try {
|
||||||
var countRequest = CountRequest.of(cr -> cr.index(getStoriesIndex()));
|
var countRequest = CountRequest.of(cr -> cr.index(getStoriesIndex()));
|
||||||
var countResponse = openSearchClient.count(countRequest);
|
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
|
// Test a simple search without sorting to see if we get results
|
||||||
if (countResponse.count() > 0) {
|
if (countResponse.count() > 0) {
|
||||||
@@ -410,7 +410,7 @@ public class OpenSearchService {
|
|||||||
.query(q -> q.matchAll(ma -> ma))
|
.query(q -> q.matchAll(ma -> ma))
|
||||||
);
|
);
|
||||||
var testResponse = openSearchClient.search(testSearch, StorySearchDto.class);
|
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().total() != null ? testResponse.hits().total().value() : 0,
|
||||||
testResponse.hits().hits().size());
|
testResponse.hits().hits().size());
|
||||||
}
|
}
|
||||||
@@ -431,10 +431,10 @@ public class OpenSearchService {
|
|||||||
String trimmedQuery = query.trim();
|
String trimmedQuery = query.trim();
|
||||||
// Handle wildcard queries
|
// Handle wildcard queries
|
||||||
if ("*".equals(trimmedQuery) || "**".equals(trimmedQuery)) {
|
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));
|
boolBuilder.must(m -> m.matchAll(ma -> ma));
|
||||||
} else {
|
} else {
|
||||||
logger.info(" Using multiMatch query for: '{}'", trimmedQuery);
|
logger.debug(" Using multiMatch query for: '{}'", trimmedQuery);
|
||||||
boolBuilder.must(m -> m
|
boolBuilder.must(m -> m
|
||||||
.multiMatch(mm -> mm
|
.multiMatch(mm -> mm
|
||||||
.query(trimmedQuery)
|
.query(trimmedQuery)
|
||||||
@@ -444,13 +444,13 @@ public class OpenSearchService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info(" Using matchAll query for empty query");
|
logger.debug(" Using matchAll query for empty query");
|
||||||
boolBuilder.must(m -> m.matchAll(ma -> ma));
|
boolBuilder.must(m -> m.matchAll(ma -> ma));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add filters
|
// Add filters
|
||||||
if (tags != null && !tags.isEmpty()) {
|
if (tags != null && !tags.isEmpty()) {
|
||||||
logger.info(" Adding tags filter: {}", tags);
|
logger.debug(" Adding tags filter: {}", tags);
|
||||||
boolBuilder.filter(f -> f
|
boolBuilder.filter(f -> f
|
||||||
.terms(t -> t
|
.terms(t -> t
|
||||||
.field("tagNames")
|
.field("tagNames")
|
||||||
@@ -460,7 +460,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (author != null && !author.trim().isEmpty() && !"null".equalsIgnoreCase(author.trim())) {
|
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
|
boolBuilder.filter(f -> f
|
||||||
.term(t -> t
|
.term(t -> t
|
||||||
.field("authorName")
|
.field("authorName")
|
||||||
@@ -472,7 +472,7 @@ public class OpenSearchService {
|
|||||||
// Series filtering is now handled by advanced seriesFilter parameter
|
// Series filtering is now handled by advanced seriesFilter parameter
|
||||||
|
|
||||||
if (minWordCount != null || maxWordCount != null) {
|
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
|
boolBuilder.filter(f -> f
|
||||||
.range(r -> {
|
.range(r -> {
|
||||||
var rangeBuilder = r.field("wordCount");
|
var rangeBuilder = r.field("wordCount");
|
||||||
@@ -488,7 +488,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (minRating != null) {
|
if (minRating != null) {
|
||||||
logger.info(" Adding rating filter: min={}", minRating);
|
logger.debug(" Adding rating filter: min={}", minRating);
|
||||||
boolBuilder.filter(f -> f
|
boolBuilder.filter(f -> f
|
||||||
.range(r -> r
|
.range(r -> r
|
||||||
.field("rating")
|
.field("rating")
|
||||||
@@ -498,7 +498,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRead != null) {
|
if (isRead != null) {
|
||||||
logger.info(" Adding isRead filter: {}", isRead);
|
logger.debug(" Adding isRead filter: {}", isRead);
|
||||||
boolBuilder.filter(f -> f
|
boolBuilder.filter(f -> f
|
||||||
.term(t -> t
|
.term(t -> t
|
||||||
.field("isRead")
|
.field("isRead")
|
||||||
@@ -508,13 +508,13 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFavorite != null) {
|
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
|
// isFavorite field is not implemented in Story entity or StorySearchDto, so ignore this filter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced date filters
|
// Advanced date filters
|
||||||
if (createdAfter != null && !createdAfter.trim().isEmpty() && !"null".equalsIgnoreCase(createdAfter.trim())) {
|
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
|
boolBuilder.filter(f -> f
|
||||||
.range(r -> r
|
.range(r -> r
|
||||||
.field("createdAt")
|
.field("createdAt")
|
||||||
@@ -524,7 +524,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (createdBefore != null && !createdBefore.trim().isEmpty() && !"null".equalsIgnoreCase(createdBefore.trim())) {
|
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
|
boolBuilder.filter(f -> f
|
||||||
.range(r -> r
|
.range(r -> r
|
||||||
.field("createdAt")
|
.field("createdAt")
|
||||||
@@ -534,7 +534,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastReadAfter != null && !lastReadAfter.trim().isEmpty() && !"null".equalsIgnoreCase(lastReadAfter.trim())) {
|
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
|
boolBuilder.filter(f -> f
|
||||||
.range(r -> r
|
.range(r -> r
|
||||||
.field("lastReadAt")
|
.field("lastReadAt")
|
||||||
@@ -544,7 +544,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastReadBefore != null && !lastReadBefore.trim().isEmpty() && !"null".equalsIgnoreCase(lastReadBefore.trim())) {
|
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
|
boolBuilder.filter(f -> f
|
||||||
.range(r -> r
|
.range(r -> r
|
||||||
.field("lastReadAt")
|
.field("lastReadAt")
|
||||||
@@ -555,7 +555,7 @@ public class OpenSearchService {
|
|||||||
|
|
||||||
// Advanced boolean filters
|
// Advanced boolean filters
|
||||||
if (unratedOnly != null && unratedOnly) {
|
if (unratedOnly != null && unratedOnly) {
|
||||||
logger.info(" Adding unratedOnly filter");
|
logger.debug(" Adding unratedOnly filter");
|
||||||
boolBuilder.filter(f -> f
|
boolBuilder.filter(f -> f
|
||||||
.bool(b -> b
|
.bool(b -> b
|
||||||
.should(s -> s.term(t -> t.field("rating").value(FieldValue.of(0))))
|
.should(s -> s.term(t -> t.field("rating").value(FieldValue.of(0))))
|
||||||
@@ -565,7 +565,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasReadingProgress != null) {
|
if (hasReadingProgress != null) {
|
||||||
logger.info(" Adding hasReadingProgress filter: {}", hasReadingProgress);
|
logger.debug(" Adding hasReadingProgress filter: {}", hasReadingProgress);
|
||||||
if (hasReadingProgress) {
|
if (hasReadingProgress) {
|
||||||
boolBuilder.filter(f -> f
|
boolBuilder.filter(f -> f
|
||||||
.range(r -> r
|
.range(r -> r
|
||||||
@@ -584,7 +584,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasCoverImage != null) {
|
if (hasCoverImage != null) {
|
||||||
logger.info(" Adding hasCoverImage filter: {}", hasCoverImage);
|
logger.debug(" Adding hasCoverImage filter: {}", hasCoverImage);
|
||||||
if (hasCoverImage) {
|
if (hasCoverImage) {
|
||||||
boolBuilder.filter(f -> f
|
boolBuilder.filter(f -> f
|
||||||
.exists(e -> e.field("coverPath"))
|
.exists(e -> e.field("coverPath"))
|
||||||
@@ -599,7 +599,7 @@ public class OpenSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sourceDomain != null && !sourceDomain.trim().isEmpty() && !"null".equalsIgnoreCase(sourceDomain.trim())) {
|
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
|
boolBuilder.filter(f -> f
|
||||||
.term(t -> t
|
.term(t -> t
|
||||||
.field("sourceDomain")
|
.field("sourceDomain")
|
||||||
@@ -610,10 +610,10 @@ public class OpenSearchService {
|
|||||||
|
|
||||||
// Reading status filter logic
|
// Reading status filter logic
|
||||||
if (readingStatus != null && !readingStatus.trim().isEmpty() && !"null".equalsIgnoreCase(readingStatus.trim()) && !"all".equalsIgnoreCase(readingStatus.trim())) {
|
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())) {
|
if ("unread".equalsIgnoreCase(readingStatus.trim())) {
|
||||||
// Simplified unread test: just check isRead = false
|
// 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
|
boolBuilder.filter(f -> f
|
||||||
.term(t -> t.field("isRead").value(FieldValue.of(false)))
|
.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)
|
// Series filter (separate from seriesFilter parameter which is handled above)
|
||||||
if (seriesFilter != null && !seriesFilter.trim().isEmpty() && !"null".equalsIgnoreCase(seriesFilter.trim())) {
|
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())) {
|
if ("standalone".equalsIgnoreCase(seriesFilter.trim())) {
|
||||||
// Stories without series: seriesName field doesn't exist or is null
|
// 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
|
boolBuilder.filter(f -> f
|
||||||
.bool(b -> b
|
.bool(b -> b
|
||||||
.mustNot(mn -> mn.exists(e -> e.field("seriesName")))
|
.mustNot(mn -> mn.exists(e -> e.field("seriesName")))
|
||||||
@@ -646,7 +646,7 @@ public class OpenSearchService {
|
|||||||
);
|
);
|
||||||
} else if ("series".equalsIgnoreCase(seriesFilter.trim())) {
|
} else if ("series".equalsIgnoreCase(seriesFilter.trim())) {
|
||||||
// Stories with series: seriesName field exists and has a value
|
// 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
|
boolBuilder.filter(f -> f
|
||||||
.exists(e -> e.field("seriesName"))
|
.exists(e -> e.field("seriesName"))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -139,6 +139,15 @@
|
|||||||
@apply max-w-full h-auto mx-auto my-6 rounded-lg shadow-sm;
|
@apply max-w-full h-auto mx-auto my-6 rounded-lg shadow-sm;
|
||||||
max-height: 80vh; /* Prevent images from being too tall */
|
max-height: 80vh; /* Prevent images from being too tall */
|
||||||
display: block;
|
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"] {
|
.reading-content img[align="left"] {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import StoryRating from '../../../components/stories/StoryRating';
|
|||||||
import TagDisplay from '../../../components/tags/TagDisplay';
|
import TagDisplay from '../../../components/tags/TagDisplay';
|
||||||
import TableOfContents from '../../../components/stories/TableOfContents';
|
import TableOfContents from '../../../components/stories/TableOfContents';
|
||||||
import { sanitizeHtml, preloadSanitizationConfig } from '../../../lib/sanitization';
|
import { sanitizeHtml, preloadSanitizationConfig } from '../../../lib/sanitization';
|
||||||
|
import { debug } from '../../../lib/debug';
|
||||||
|
|
||||||
// Memoized content component that only re-renders when content changes
|
// Memoized content component that only re-renders when content changes
|
||||||
const StoryContent = memo(({
|
const StoryContent = memo(({
|
||||||
@@ -20,13 +21,50 @@ const StoryContent = memo(({
|
|||||||
content: string;
|
content: string;
|
||||||
contentRef: React.RefObject<HTMLDivElement>;
|
contentRef: React.RefObject<HTMLDivElement>;
|
||||||
}) => {
|
}) => {
|
||||||
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
className="reading-content"
|
className="reading-content"
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
style={{
|
||||||
|
// Prevent layout shifts that might cause image reloads
|
||||||
|
minHeight: '100vh',
|
||||||
|
contain: 'layout style'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -112,14 +150,14 @@ export default function StoryReadingPage() {
|
|||||||
// Debounced function to save reading position
|
// Debounced function to save reading position
|
||||||
const saveReadingPosition = useCallback(async (position: number) => {
|
const saveReadingPosition = useCallback(async (position: number) => {
|
||||||
if (!story || position === story.readingPosition) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Saving reading position:', position, 'for story:', story.id);
|
debug.log('Saving reading position:', position, 'for story:', story.id);
|
||||||
try {
|
try {
|
||||||
const updatedStory = await storyApi.updateReadingProgress(story.id, position);
|
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);
|
setStory(prev => prev ? { ...prev, readingPosition: position, lastReadAt: updatedStory.lastReadAt } : null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save reading position:', error);
|
console.error('Failed to save reading position:', error);
|
||||||
@@ -200,12 +238,12 @@ export default function StoryReadingPage() {
|
|||||||
if (story && sanitizedContent && !hasScrolledToPosition) {
|
if (story && sanitizedContent && !hasScrolledToPosition) {
|
||||||
// Use a small delay to ensure content is rendered
|
// Use a small delay to ensure content is rendered
|
||||||
const timeout = setTimeout(() => {
|
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)
|
// Check if there's a hash in the URL (for TOC navigation)
|
||||||
const hash = window.location.hash.substring(1);
|
const hash = window.location.hash.substring(1);
|
||||||
if (hash && hash.startsWith('heading-')) {
|
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);
|
const element = document.getElementById(hash);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
@@ -219,13 +257,13 @@ export default function StoryReadingPage() {
|
|||||||
|
|
||||||
// Otherwise, use saved reading position
|
// Otherwise, use saved reading position
|
||||||
if (story.readingPosition && story.readingPosition > 0) {
|
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);
|
const initialPercentage = calculateReadingPercentage(story.readingPosition);
|
||||||
setReadingPercentage(initialPercentage);
|
setReadingPercentage(initialPercentage);
|
||||||
scrollToCharacterPosition(story.readingPosition);
|
scrollToCharacterPosition(story.readingPosition);
|
||||||
} else {
|
} else {
|
||||||
// Even if there's no saved position, mark as ready for tracking
|
// 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);
|
setReadingPercentage(0);
|
||||||
setHasScrolledToPosition(true);
|
setHasScrolledToPosition(true);
|
||||||
}
|
}
|
||||||
@@ -238,8 +276,14 @@ export default function StoryReadingPage() {
|
|||||||
// Track reading progress and save position
|
// Track reading progress and save position
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let ticking = false;
|
let ticking = false;
|
||||||
|
let scrollEventCount = 0;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
scrollEventCount++;
|
||||||
|
if (scrollEventCount % 10 === 0) {
|
||||||
|
debug.log('📜 Scroll event #', scrollEventCount, 'at', Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
if (!ticking) {
|
if (!ticking) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const article = document.querySelector('[data-reading-content]') as HTMLElement;
|
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)
|
// Trigger end detection if user is near bottom AND (has high progress OR story content is fully visible)
|
||||||
if (nearBottom && (highProgress || storyContentFullyVisible) && !hasReachedEnd && hasScrolledToPosition) {
|
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);
|
setHasReachedEnd(true);
|
||||||
setShowEndOfStoryPopup(true);
|
setShowEndOfStoryPopup(true);
|
||||||
}
|
}
|
||||||
@@ -287,11 +331,11 @@ export default function StoryReadingPage() {
|
|||||||
if (hasScrolledToPosition) { // Only save after initial auto-scroll
|
if (hasScrolledToPosition) { // Only save after initial auto-scroll
|
||||||
const characterPosition = getCharacterPositionFromScroll();
|
const characterPosition = getCharacterPositionFromScroll();
|
||||||
const percentage = calculateReadingPercentage(characterPosition);
|
const percentage = calculateReadingPercentage(characterPosition);
|
||||||
console.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage);
|
debug.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage);
|
||||||
setReadingPercentage(percentage);
|
setReadingPercentage(percentage);
|
||||||
debouncedSavePosition(characterPosition);
|
debouncedSavePosition(characterPosition);
|
||||||
} else {
|
} else {
|
||||||
console.log('Scroll detected but not ready for tracking yet');
|
debug.log('Scroll detected but not ready for tracking yet');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ticking = false;
|
ticking = false;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { PortableText } from '@portabletext/react';
|
|||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import { sanitizeHtmlSync } from '../../lib/sanitization';
|
import { sanitizeHtmlSync } from '../../lib/sanitization';
|
||||||
import { editorSchema } from '../../lib/portabletext/editorSchema';
|
import { editorSchema } from '../../lib/portabletext/editorSchema';
|
||||||
|
import { debug } from '../../lib/debug';
|
||||||
|
|
||||||
interface PortableTextEditorProps {
|
interface PortableTextEditorProps {
|
||||||
value: string; // HTML value for compatibility - will be converted
|
value: string; // HTML value for compatibility - will be converted
|
||||||
@@ -394,13 +395,13 @@ function EditorContent({
|
|||||||
|
|
||||||
// Sync HTML value with prop changes
|
// Sync HTML value with prop changes
|
||||||
useEffect(() => {
|
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));
|
setPortableTextValue(htmlToPortableTextBlocks(value));
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
// Debug: log when portableTextValue changes
|
// Debug: log when portableTextValue changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue });
|
debug.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue });
|
||||||
}, [portableTextValue]);
|
}, [portableTextValue]);
|
||||||
|
|
||||||
// Add a ref to the editor container for direct paste handling
|
// 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
|
// Global paste event listener to catch ALL paste events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleGlobalPaste = (event: ClipboardEvent) => {
|
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
|
// Check if the paste is happening within our editor
|
||||||
const target = event.target as Element;
|
const target = event.target as Element;
|
||||||
const isInEditor = editorContainerRef.current?.contains(target);
|
const isInEditor = editorContainerRef.current?.contains(target);
|
||||||
|
|
||||||
console.log('📋 Paste details:', {
|
debug.log('📋 Paste details:', {
|
||||||
isInEditor,
|
isInEditor,
|
||||||
targetTag: target?.tagName,
|
targetTag: target?.tagName,
|
||||||
targetClasses: target?.className,
|
targetClasses: target?.className,
|
||||||
@@ -426,7 +427,7 @@ function EditorContent({
|
|||||||
const htmlData = event.clipboardData.getData('text/html');
|
const htmlData = event.clipboardData.getData('text/html');
|
||||||
const textData = event.clipboardData.getData('text/plain');
|
const textData = event.clipboardData.getData('text/plain');
|
||||||
|
|
||||||
console.log('📋 Clipboard contents:', {
|
debug.log('📋 Clipboard contents:', {
|
||||||
htmlLength: htmlData.length,
|
htmlLength: htmlData.length,
|
||||||
textLength: textData.length,
|
textLength: textData.length,
|
||||||
hasImages: htmlData.includes('<img'),
|
hasImages: htmlData.includes('<img'),
|
||||||
@@ -434,7 +435,7 @@ function EditorContent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (htmlData && htmlData.includes('<img')) {
|
if (htmlData && htmlData.includes('<img')) {
|
||||||
console.log('📋 Images detected in paste! Attempting to process...');
|
debug.log('📋 Images detected in paste! Attempting to process...');
|
||||||
|
|
||||||
// Prevent default paste to handle it completely ourselves
|
// Prevent default paste to handle it completely ourselves
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -443,7 +444,7 @@ function EditorContent({
|
|||||||
// Convert the pasted HTML to our blocks maintaining order
|
// Convert the pasted HTML to our blocks maintaining order
|
||||||
const pastedBlocks = htmlToPortableTextBlocks(htmlData);
|
const pastedBlocks = htmlToPortableTextBlocks(htmlData);
|
||||||
|
|
||||||
console.log('📋 Converted blocks:', pastedBlocks.map(block => ({
|
debug.log('📋 Converted blocks:', pastedBlocks.map(block => ({
|
||||||
type: block._type,
|
type: block._type,
|
||||||
key: block._key,
|
key: block._key,
|
||||||
...(block._type === 'image' ? { src: (block as any).src, alt: (block as any).alt } : {}),
|
...(block._type === 'image' ? { src: (block as any).src, alt: (block as any).alt } : {}),
|
||||||
@@ -457,7 +458,7 @@ function EditorContent({
|
|||||||
const updatedBlocks = [...prev, ...pastedBlocks];
|
const updatedBlocks = [...prev, ...pastedBlocks];
|
||||||
const html = portableTextToHtml(updatedBlocks);
|
const html = portableTextToHtml(updatedBlocks);
|
||||||
onChange(html);
|
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;
|
return updatedBlocks;
|
||||||
});
|
});
|
||||||
}, 10);
|
}, 10);
|
||||||
@@ -476,7 +477,7 @@ function EditorContent({
|
|||||||
|
|
||||||
// Handle paste events directly on the editor container (backup approach)
|
// Handle paste events directly on the editor container (backup approach)
|
||||||
const handleContainerPaste = useCallback((_event: React.ClipboardEvent) => {
|
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
|
// This might not be reached if global handler prevents default
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -526,11 +527,11 @@ function EditorContent({
|
|||||||
const renderBlock: RenderBlockFunction = useCallback((props) => {
|
const renderBlock: RenderBlockFunction = useCallback((props) => {
|
||||||
const { schemaType, value, children } = 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
|
// Handle image blocks
|
||||||
if (schemaType.name === 'image' && isImageBlock(value)) {
|
if (schemaType.name === 'image' && isImageBlock(value)) {
|
||||||
console.log('🖼️ Rendering image block:', value);
|
debug.log('🖼️ Rendering image block:', value);
|
||||||
return (
|
return (
|
||||||
<div className="my-4 p-3 border border-dashed border-gray-300 rounded-lg bg-gray-50">
|
<div className="my-4 p-3 border border-dashed border-gray-300 rounded-lg bg-gray-50">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
@@ -654,7 +655,7 @@ export default function PortableTextEditorNew({
|
|||||||
storyId,
|
storyId,
|
||||||
enableImageProcessing = false
|
enableImageProcessing = false
|
||||||
}: PortableTextEditorProps) {
|
}: PortableTextEditorProps) {
|
||||||
console.log('🎯 Portable Text Editor loaded!', {
|
debug.log('🎯 Portable Text Editor loaded!', {
|
||||||
valueLength: value?.length,
|
valueLength: value?.length,
|
||||||
enableImageProcessing,
|
enableImageProcessing,
|
||||||
hasStoryId: !!storyId
|
hasStoryId: !!storyId
|
||||||
|
|||||||
90
frontend/src/lib/debug.ts
Normal file
90
frontend/src/lib/debug.ts
Normal file
@@ -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.');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { configApi } from './api';
|
import { configApi } from './api';
|
||||||
|
import { debug } from './debug';
|
||||||
|
|
||||||
interface SanitizationConfig {
|
interface SanitizationConfig {
|
||||||
allowedTags: string[];
|
allowedTags: string[];
|
||||||
@@ -28,7 +29,7 @@ function filterCssProperties(styleValue: string, allowedProperties: string[]): s
|
|||||||
const isAllowed = allowedProperties.includes(property);
|
const isAllowed = allowedProperties.includes(property);
|
||||||
|
|
||||||
if (!isAllowed) {
|
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;
|
return isAllowed;
|
||||||
@@ -37,9 +38,9 @@ function filterCssProperties(styleValue: string, allowedProperties: string[]): s
|
|||||||
const result = filteredDeclarations.join('; ');
|
const result = filteredDeclarations.join('; ');
|
||||||
|
|
||||||
if (declarations.length !== filteredDeclarations.length) {
|
if (declarations.length !== filteredDeclarations.length) {
|
||||||
console.log(`CSS filtering: ${declarations.length} -> ${filteredDeclarations.length} properties`);
|
debug.log(`CSS filtering: ${declarations.length} -> ${filteredDeclarations.length} properties`);
|
||||||
console.log('Original:', styleValue);
|
debug.log('Original:', styleValue);
|
||||||
console.log('Filtered:', result);
|
debug.log('Filtered:', result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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 we don't have cached config but there's an ongoing request, wait for it
|
||||||
if (configPromise) {
|
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 {
|
} else {
|
||||||
// No config and no ongoing request - try to load it for next time
|
// 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');
|
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
|
// 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 = [
|
const fallbackAllowedCssProperties = [
|
||||||
'color', 'font-size', 'font-weight',
|
'color', 'font-size', 'font-weight',
|
||||||
'font-style', 'text-align', 'text-decoration', 'margin',
|
'font-style', 'text-align', 'text-decoration', 'margin',
|
||||||
|
|||||||
Reference in New Issue
Block a user