diff --git a/README.md b/README.md index 3942256..965c27f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, ** - **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion - **πŸ“¦ Export Functionality**: Export all posts as tar.gz archive (Docker only) - **πŸ’Ύ Smart Caching**: RAM-based caching system for instant post retrieval +- **πŸ”§ VS Code-Style Editor**: Monaco editor with YAML frontmatter support and live preview +- **πŸ”„ Force Reparse**: Manual cache clearing and post reparsing for immediate updates +- **πŸ“ Reliable Directory Scanning**: Robust file system traversal with error handling --- @@ -43,13 +46,15 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, ** markdownblog/ β”œβ”€β”€ markdown_backend/ # Rust backend for markdown processing β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ main.rs # CLI interface and command handling -β”‚ β”‚ └── markdown.rs # Markdown parsing, caching, and file watching -β”‚ β”œβ”€β”€ Cargo.toml # Rust dependencies and configuration -β”‚ └── target/ # Compiled Rust binaries +β”‚ β”‚ β”œβ”€β”€ main.rs # CLI interface and command handling +β”‚ β”‚ └── markdown.rs # Markdown parsing, caching, and file watching +β”‚ β”œβ”€β”€ Cargo.toml # Rust dependencies and configuration +β”‚ └── target/ # Compiled Rust binaries β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ app/ # Next.js 14 App Router +β”‚ β”œβ”€β”€ app/ # Next.js 14 App Router β”‚ β”‚ β”œβ”€β”€ admin/ # Admin dashboard pages +β”‚ β”‚ β”‚ β”œβ”€β”€ editor/ # VS Code-style editor +β”‚ β”‚ β”‚ β”‚ └── page.tsx # Markdown editor with Monaco β”‚ β”‚ β”‚ β”œβ”€β”€ manage/ # Content management interface β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # Manage posts and folders β”‚ β”‚ β”‚ β”‚ └── rust-status/ # Rust backend monitoring @@ -84,8 +89,12 @@ markdownblog/ β”‚ β”‚ β”‚ └── posts/ # Public post API β”‚ β”‚ β”‚ β”œβ”€β”€ [slug]/ # Dynamic post API routes β”‚ β”‚ β”‚ β”‚ └── route.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ preview/ # Markdown preview API +β”‚ β”‚ β”‚ β”‚ └── route.ts β”‚ β”‚ β”‚ β”œβ”€β”€ stream/ # Server-Sent Events for real-time updates β”‚ β”‚ β”‚ β”‚ └── route.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ webhook/ # Webhook endpoint +β”‚ β”‚ β”‚ β”‚ └── route.ts β”‚ β”‚ β”‚ └── route.ts # List all posts β”‚ β”‚ β”œβ”€β”€ posts/ # Blog post pages β”‚ β”‚ β”‚ └── [...slug]/ # Dynamic post routing (catch-all) @@ -97,15 +106,15 @@ markdownblog/ β”‚ β”‚ β”œβ”€β”€ highlight-github.css # Code syntax highlighting styles β”‚ β”‚ β”œβ”€β”€ layout.tsx # Root layout with metadata β”‚ β”‚ β”œβ”€β”€ MobileNav.tsx # Mobile navigation component +β”‚ β”‚ β”œβ”€β”€ monaco-vim.d.ts # Monaco Vim typings β”‚ β”‚ └── page.tsx # Homepage with post listing β”‚ └── lib/ # Utility libraries β”‚ └── postsDirectory.ts # Post directory management and Rust integration β”œβ”€β”€ posts/ # Markdown blog posts storage -β”‚ β”œβ”€β”€ pinned.json # Pinned posts configuration -β”‚ β”œβ”€β”€ welcome.md # Welcome post with frontmatter -β”‚ β”œβ”€β”€ mdtest.md # Test post with various markdown features -β”‚ β”œβ”€β”€ anchor-test.md # Test post for anchor linking -β”‚ └── ii/ # Example nested folder structure +β”‚ β”œβ”€β”€ about.md +β”‚ β”œβ”€β”€ welcome.md +β”‚ └── assets/ +β”‚ └── peta.png β”œβ”€β”€ public/ # Static assets β”‚ β”œβ”€β”€ android-chrome-192x192.png β”‚ β”œβ”€β”€ android-chrome-512x512.png @@ -115,19 +124,19 @@ markdownblog/ β”‚ β”œβ”€β”€ favicon.ico β”‚ └── site.webmanifest β”œβ”€β”€ electron/ # Desktop application -β”‚ └── main.js # Electron main process configuration +β”‚ └── main.js # Electron main process configuration β”œβ”€β”€ Dockerfile # Docker container configuration β”œβ”€β”€ docker.sh # Docker deployment script -β”œβ”€β”€ entrypoint.sh # Container entrypoint script -β”œβ”€β”€ run-local-backend.sh # Local Rust backend runner -β”œβ”€β”€ next-env.d.ts # Next.js TypeScript definitions -β”œβ”€β”€ next.config.js # Next.js configuration -β”œβ”€β”€ package-lock.json # npm lock file -β”œβ”€β”€ package.json # Dependencies and scripts -β”œβ”€β”€ postcss.config.js # PostCSS configuration -β”œβ”€β”€ tailwind.config.js # Tailwind CSS configuration -β”œβ”€β”€ tsconfig.json # TypeScript configuration -└── LICENSE # MIT License +β”œβ”€β”€ entrypoint.sh # Container entrypoint script +β”œβ”€β”€ run-local-backend.sh # Local Rust backend runner +β”œβ”€β”€ next-env.d.ts # Next.js TypeScript definitions +β”œβ”€β”€ next.config.js # Next.js configuration +β”œβ”€β”€ package-lock.json # npm lock file +β”œβ”€β”€ package.json # Dependencies and scripts +β”œβ”€β”€ postcss.config.js # PostCSS configuration +β”œβ”€β”€ tailwind.config.js # Tailwind CSS configuration +β”œβ”€β”€ tsconfig.json # TypeScript configuration +└── LICENSE # MIT License ``` ### Key Components @@ -329,6 +338,9 @@ console.log("Hello, World!"); - **πŸ“¦ Export Posts**: Download all posts as archive (Docker only) - **πŸ“Š Rust Status**: Monitor parser performance, logs, and health - **πŸ” Log Management**: View, filter, and clear parser logs +- **πŸ”§ VS Code Editor**: Monaco-based editor with YAML frontmatter preservation +- **πŸ”„ Force Reparse**: Manual cache clearing and post reparsing +- **πŸ“ Reliable Scanning**: Enhanced directory traversal with error recovery ### Security @@ -350,6 +362,9 @@ console.log("Hello, World!"); - **πŸ“ Recursive Scanning**: Efficient folder traversal and file discovery - **πŸ’Ύ Smart Caching**: RAM-based caching with disk persistence - **πŸ“Š Performance Monitoring**: Real-time metrics and logging +- **πŸ”„ Force Reparse**: Manual cache invalidation and post reparsing +- **πŸ“ Reliable Directory Scanning**: Robust error handling and recovery +- **πŸ”§ Single Post Reparse**: Efficient individual post cache clearing ### Real-Time Updates @@ -358,6 +373,16 @@ console.log("Hello, World!"); - **⚑ Instant Updates**: Sub-second response to file modifications - **πŸ”„ Fallback Polling**: Graceful degradation if SSE fails +### VS Code-Style Editor + +- **πŸ”§ Monaco Editor**: Professional code editor with syntax highlighting +- **πŸ“„ YAML Frontmatter**: Preserved and editable at the top of files +- **πŸ‘οΈ Live Preview**: Real-time Markdown rendering +- **πŸ’Ύ Save & Reparse**: Automatic cache clearing and post reparsing +- **⌨️ Vim Mode**: Optional Vim keybindings for power users +- **πŸ“± Responsive Design**: Works on desktop and mobile devices +- **🎨 Custom Styling**: JetBrains Mono font and VS Code-like appearance + --- ## 🎨 Customization @@ -398,6 +423,8 @@ cargo build --release # Build optimized binary cargo run -- watch # Watch for file changes cargo run -- logs # View parser logs cargo run -- checkhealth # Check backend health +cargo run -- reinterpret-all # Force reparse all posts +cargo run -- reparse-post # Force reparse single post ``` --- @@ -434,7 +461,43 @@ MIT License - see [LICENSE](LICENSE) file for details. - **File watching issues**: Check file permissions and inotify limits - **Performance issues**: Monitor logs via admin interface - **Cache problems**: Clear cache via admin interface or restart +- **Directory scanning errors**: Check file permissions and hidden files +- **Reparse failures**: Verify post slugs and file existence +- **Memory issues**: Monitor cache size and clear if necessary ### Support -For issues and questions, please check the project structure and API documentation in the codebase. The admin interface includes comprehensive monitoring tools for the Rust backend. \ No newline at end of file +For issues and questions, please check the project structure and API documentation in the codebase. The admin interface includes comprehensive monitoring tools for the Rust backend. + +--- + +## πŸ†• Recent Improvements (Latest) + +### Rust Backend Enhancements + +- **πŸ”„ Force Reparse Commands**: New CLI commands for manual cache invalidation + - `reinterpret-all`: Clear all caches and reparse every post + - `reparse-post `: Clear cache for specific post and reparse +- **πŸ“ Reliable Directory Scanning**: Enhanced file system traversal with: + - Hidden file filtering (skips `.` files) + - Graceful error recovery for inaccessible files + - Detailed logging of scanning process + - Automatic directory creation if missing +- **πŸ’Ύ Improved Cache Management**: Better cache directory handling and persistence +- **πŸ“Š Enhanced Logging**: Comprehensive logging for debugging and monitoring + +### Editor Improvements + +- **πŸ”§ VS Code-Style Interface**: Monaco editor with professional features +- **πŸ“„ YAML Frontmatter Preservation**: Frontmatter stays at top and remains editable +- **πŸ’Ύ Save & Reparse Integration**: Automatic Rust backend integration on save +- **πŸ‘οΈ Live Preview**: Real-time Markdown rendering without frontmatter +- **⌨️ Vim Mode Support**: Optional Vim keybindings for power users +- **πŸ“± Mobile Responsive**: Works seamlessly on all device sizes + +### Admin Panel Enhancements + +- **πŸ”„ Force Reparse Button**: One-click cache clearing and post reparsing +- **πŸ“Š Enhanced Rust Status**: Real-time parser performance monitoring +- **πŸ” Improved Log Management**: Better filtering and search capabilities +- **πŸ“ Directory Health Monitoring**: Comprehensive file system diagnostics \ No newline at end of file diff --git a/markdown_backend/src/main.rs b/markdown_backend/src/main.rs index 9fa1e04..75ad3fa 100644 --- a/markdown_backend/src/main.rs +++ b/markdown_backend/src/main.rs @@ -1,7 +1,17 @@ #[warn(unused_imports)] use clap::{Parser, Subcommand}; mod markdown; -use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts, get_parser_logs, clear_parser_logs, load_parser_logs_from_disk}; +use markdown::{ + get_all_posts, + get_post_by_slug, + get_posts_by_tag, + watch_posts, + get_parser_logs, + clear_parser_logs, + load_parser_logs_from_disk, + force_reinterpret_all_posts, + force_reparse_single_post +}; use serde_json; use std::fs; use std::io; @@ -44,6 +54,12 @@ enum Commands { Logs, /// Clear parser logs ClearLogs, + /// Force reinterpret all posts (clear cache and re-parse) + ReinterpretAll, + /// Force reparse a single post (clear cache and re-parse) + ReparsePost { + slug: String, + }, /// Parse markdown from file or stdin Parse { #[arg(long)] @@ -111,6 +127,35 @@ fn main() { clear_parser_logs(); println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Logs cleared"})).unwrap()); } + Commands::ReinterpretAll => { + match force_reinterpret_all_posts() { + Ok(posts) => { + println!("{}", serde_json::to_string(&serde_json::json!({ + "success": true, + "message": format!("All posts reinterpreted successfully. Processed {} posts.", posts.len()) + })).unwrap()); + } + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + } + } + Commands::ReparsePost { slug } => { + match force_reparse_single_post(slug) { + Ok(post) => { + println!("{}", serde_json::to_string(&serde_json::json!({ + "success": true, + "message": format!("Post '{}' reparsed successfully", slug), + "post": post + })).unwrap()); + } + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + } + } Commands::Parse { file, stdin, ast } => { let content = if *stdin { let mut buffer = String::new(); @@ -139,4 +184,4 @@ fn main() { } } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/markdown_backend/src/markdown.rs b/markdown_backend/src/markdown.rs index 4b07571..0a9588b 100644 --- a/markdown_backend/src/markdown.rs +++ b/markdown_backend/src/markdown.rs @@ -180,6 +180,18 @@ static AMMONIA: Lazy> = Lazy::new(|| { }); // Helper functions +fn ensure_cache_directory() { + let cache_dir = PathBuf::from("./cache"); + if !cache_dir.exists() { + if let Err(e) = fs::create_dir_all(&cache_dir) { + eprintln!("Failed to create cache directory: {}", e); + add_log("error", &format!("Failed to create cache directory: {}", e), None, None); + } else { + add_log("info", "Created cache directory: ./cache", None, None); + } + } +} + fn get_posts_directory() -> PathBuf { let is_docker = std::env::var("DOCKER_CONTAINER").is_ok() || std::env::var("KUBERNETES_SERVICE_HOST").is_ok() @@ -207,29 +219,101 @@ fn get_posts_directory() -> PathBuf { for candidate in candidates.iter() { let path = PathBuf::from(candidate); if path.exists() && path.is_dir() { + add_log("info", &format!("Using posts directory: {:?}", path), None, None); return path; } } - // Fallback: default to ./posts - PathBuf::from("./posts") + + // Fallback: create ./posts if it doesn't exist + let fallback_path = PathBuf::from("./posts"); + if !fallback_path.exists() { + if let Err(e) = fs::create_dir_all(&fallback_path) { + add_log("error", &format!("Failed to create posts directory: {}", e), None, None); + } else { + add_log("info", "Created posts directory: ./posts", None, None); + } + } + fallback_path } -// Function to find Markdown files. -// This will scan Directories recursively +// Function to find Markdown files with improved reliability fn find_markdown_files(dir: &Path) -> std::io::Result> { let mut files = Vec::new(); - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - files.extend(find_markdown_files(&path)?); - } else if path.extension().map(|e| e == "md").unwrap_or(false) { - files.push(path); + let mut errors = Vec::new(); + + if !dir.exists() { + let error_msg = format!("Directory does not exist: {:?}", dir); + add_log("error", &error_msg, None, None); + return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg)); + } + + if !dir.is_dir() { + let error_msg = format!("Path is not a directory: {:?}", dir); + add_log("error", &error_msg, None, None); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, error_msg)); + } + + // Try to read directory with retry logic + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(e) => { + add_log("error", &format!("Failed to read directory {:?}: {}", dir, e), None, None); + return Err(e); + } + }; + + for entry_result in entries { + match entry_result { + Ok(entry) => { + let path = entry.path(); + + // Skip hidden files and directories + if let Some(name) = path.file_name() { + if name.to_string_lossy().starts_with('.') { + continue; + } + } + + if path.is_dir() { + // Recursively scan subdirectories + match find_markdown_files(&path) { + Ok(subfiles) => files.extend(subfiles), + Err(e) => { + let error_msg = format!("Error scanning subdirectory {:?}: {}", path, e); + add_log("warning", &error_msg, None, None); + errors.push(error_msg); + } + } + } else if path.extension().map(|e| e == "md").unwrap_or(false) { + // Verify the file is readable + match fs::metadata(&path) { + Ok(metadata) => { + if metadata.is_file() { + files.push(path); + } + } + Err(e) => { + let error_msg = format!("Cannot access file {:?}: {}", path, e); + add_log("warning", &error_msg, None, None); + errors.push(error_msg); + } + } + } + } + Err(e) => { + let error_msg = format!("Error reading directory entry: {}", e); + add_log("warning", &error_msg, None, None); + errors.push(error_msg); } } } + + // Log summary + add_log("info", &format!("Found {} markdown files in {:?}", files.len(), dir), None, None); + if !errors.is_empty() { + add_log("warning", &format!("Encountered {} errors during directory scan", errors.len()), None, None); + } + Ok(files) } @@ -372,7 +456,7 @@ fn add_log(level: &str, message: &str, slug: Option<&str>, details: Option<&str> } fn save_parser_logs_to_disk_inner(logs: &VecDeque) -> std::io::Result<()> { - let _ = std::fs::create_dir_all("./cache"); + ensure_cache_directory(); let logs_vec: Vec<_> = logs.iter().cloned().collect(); let json = serde_json::to_string(&logs_vec)?; std::fs::write(PARSER_LOGS_PATH, json)?; @@ -654,12 +738,11 @@ pub fn load_post_cache_from_disk() { } pub fn save_post_cache_to_disk() { + ensure_cache_directory(); if let Ok(map) = serde_json::to_string(&*POST_CACHE.read().unwrap()) { - let _ = fs::create_dir_all("./cache"); let _ = fs::write(POSTS_CACHE_PATH, map); } if let Ok(map) = serde_json::to_string(&*POST_STATS.read().unwrap()) { - let _ = fs::create_dir_all("./cache"); let _ = fs::write(POST_STATS_PATH, map); } } @@ -739,7 +822,90 @@ pub fn get_parser_logs() -> Vec { } pub fn clear_parser_logs() { - let mut logs = PARSER_LOGS.write().unwrap(); - logs.clear(); - let _ = std::fs::remove_file(PARSER_LOGS_PATH); + PARSER_LOGS.write().unwrap().clear(); + if let Err(e) = save_parser_logs_to_disk_inner(&VecDeque::new()) { + eprintln!("Failed to save empty logs to disk: {}", e); + } +} + +// Force reinterpret all posts by clearing cache and re-parsing +pub fn force_reinterpret_all_posts() -> Result, Box> { + add_log("info", "Starting force reinterpret of all posts", None, None); + + // Clear all caches + POST_CACHE.write().unwrap().clear(); + ALL_POSTS_CACHE.write().unwrap().take(); + POST_STATS.write().unwrap().clear(); + + add_log("info", "Cleared all caches", None, None); + + // Get posts directory and find all markdown files + let posts_dir = get_posts_directory(); + let markdown_files = find_markdown_files(&posts_dir)?; + + add_log("info", &format!("Found {} markdown files to reinterpret", markdown_files.len()), None, None); + + let mut posts = Vec::new(); + let mut success_count = 0; + let mut error_count = 0; + + for file_path in markdown_files { + let slug = path_to_slug(&file_path, &posts_dir); + match get_post_by_slug(&slug) { + Ok(post) => { + posts.push(post); + success_count += 1; + add_log("info", &format!("Successfully reinterpreted: {}", slug), Some(&slug), None); + } + Err(e) => { + error_count += 1; + add_log("error", &format!("Failed to reinterpret {}: {}", slug, e), Some(&slug), None); + } + } + } + + // Update the all posts cache + ALL_POSTS_CACHE.write().unwrap().replace(posts.clone()); + + // Save cache to disk + save_post_cache_to_disk(); + + add_log("info", &format!("Force reinterpret completed. Success: {}, Errors: {}", success_count, error_count), None, None); + + Ok(posts) +} + +// Force reparse a single post by clearing its cache and re-parsing +pub fn force_reparse_single_post(slug: &str) -> Result> { + add_log("info", &format!("Starting force reparse of post: {}", slug), Some(slug), None); + + // Clear this specific post from all caches + POST_CACHE.write().unwrap().remove(slug); + POST_STATS.write().unwrap().remove(slug); + + // Clear the all posts cache since it might contain this post + ALL_POSTS_CACHE.write().unwrap().take(); + + add_log("info", &format!("Cleared cache for post: {}", slug), Some(slug), None); + + // Re-parse the post + let post = get_post_by_slug(slug)?; + + // Update the all posts cache with the new post + let mut all_posts_cache = ALL_POSTS_CACHE.write().unwrap(); + if let Some(ref mut posts) = *all_posts_cache { + // Remove old version if it exists + posts.retain(|p| p.slug != slug); + // Add new version + posts.push(post.clone()); + // Sort by creation date + posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + } + + // Save cache to disk + save_post_cache_to_disk(); + + add_log("info", &format!("Successfully reparsed post: {}", slug), Some(slug), None); + + Ok(post) } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 44edc60..e29fdd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "markdownblog", "version": "0.1.0", "dependencies": { + "@fontsource/jetbrains-mono": "^5.2.6", + "@monaco-editor/react": "^4.7.0", "@tailwindcss/typography": "^0.5.16", "@types/node": "^20.11.24", "@types/react": "^18.2.61", @@ -23,8 +25,11 @@ "emoji-picker-react": "^4.12.2", "gray-matter": "^4.0.3", "highlight.js": "^11.11.1", + "isomorphic-dompurify": "^2.25.0", "jsdom": "^24.0.0", "marked": "^12.0.0", + "monaco-editor": "^0.52.2", + "monaco-vim": "^0.4.2", "next": "14.1.0", "pm2": "^6.0.8", "postcss": "^8.4.35", @@ -304,6 +309,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.6.tgz", + "integrity": "sha512-nz//dBr99hXZmHp10wgNI00qThWImkzRR5PQjvRM+rpmuHO5rYBJCqPPWufidCvmkkryXx/GOP/lgqsM3R3Org==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -542,6 +556,29 @@ "node": ">=10" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@next/env": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", @@ -1176,19 +1213,6 @@ "parse5": "^7.0.0" } }, - "node_modules/@types/jsdom/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -5147,6 +5171,92 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isomorphic-dompurify": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz", + "integrity": "sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.2.6", + "jsdom": "^26.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/isomorphic-dompurify/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/isomorphic-dompurify/node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/isomorphic-dompurify/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -5291,18 +5401,6 @@ "node": ">= 14" } }, - "node_modules/jsdom/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/jsdom/node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -5664,6 +5762,21 @@ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT" + }, + "node_modules/monaco-vim": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.2.tgz", + "integrity": "sha512-rdbQC3O2rmpwX2Orzig/6gZjZfH7q7TIeB+uEl49sa+QyNm3jCKJOw5mwxBdFzTqbrPD+URfg6A2lEkuL5kymw==", + "license": "MIT", + "peerDependencies": { + "monaco-editor": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6263,6 +6376,18 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7590,6 +7715,12 @@ "dev": true, "license": "MIT" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8189,6 +8320,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 7893274..c234607 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "electron-dev": "concurrently \"npm run dev\" \"npm run electron\"" }, "dependencies": { + "@fontsource/jetbrains-mono": "^5.2.6", + "@monaco-editor/react": "^4.7.0", "@tailwindcss/typography": "^0.5.16", "@types/node": "^20.11.24", "@types/react": "^18.2.61", @@ -26,8 +28,11 @@ "emoji-picker-react": "^4.12.2", "gray-matter": "^4.0.3", "highlight.js": "^11.11.1", + "isomorphic-dompurify": "^2.25.0", "jsdom": "^24.0.0", "marked": "^12.0.0", + "monaco-editor": "^0.52.2", + "monaco-vim": "^0.4.2", "next": "14.1.0", "pm2": "^6.0.8", "postcss": "^8.4.35", diff --git a/posts/about.md b/posts/about.md new file mode 100644 index 0000000..a7ac660 --- /dev/null +++ b/posts/about.md @@ -0,0 +1,40 @@ +--- +title: About Me +date: 2025-07-04 +tags: [about, profile] +author: rattatwinko +summary: This is the about page +--- + +# About Me + +_**I am rattatwinko**_ + +I created this Project because of the lack of Blog's that use Markdown. +It really is sad that there are so many blog platforms which are shit. + +## What I used: +- TypeScript +- Next.JS +- Rust +- Monaco (for a beautiful Editing experience) +- More shit which you can check out in the Repo + +## What I do + +School. +Coding. +Not more not less. + +### Socials + + + +
+ +
+ +
+ +
+ diff --git a/posts/assets/peta.png b/posts/assets/peta.png new file mode 100644 index 0000000..5716be0 Binary files /dev/null and b/posts/assets/peta.png differ diff --git a/src/app/AboutButton.tsx b/src/app/AboutButton.tsx index b313436..760f1d6 100644 --- a/src/app/AboutButton.tsx +++ b/src/app/AboutButton.tsx @@ -1,5 +1,6 @@ 'use client'; import BadgeButton from './BadgeButton'; +import { useRouter } from 'next/navigation'; const InfoIcon = (
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..2a1b5ac --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,98 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +interface Post { + slug: string; + title: string; + date: string; + tags: string[]; + summary?: string; + content: string; + createdAt: string; + author: string; +} + +export default function AboutPage() { + const [post, setPost] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const loadAbout = async () => { + try { + setLoading(true); + setError(null); + const response = await fetch("/api/posts/about"); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + setPost(data); + } catch (error) { + setError(error instanceof Error ? error.message : "Unknown error"); + } finally { + setLoading(false); + } + }; + loadAbout(); + }, []); + + if (loading) { + return ( +
+
+
+

Lade About...

+
+
+ ); + } + + if (error) { + return ( +
+
+
⚠️
+

Fehler beim Laden

+

{error}

+
+
+ ); + } + + if (!post) { + return ( +
+
+
❌
+

About nicht gefunden

+

Die About-Seite konnte nicht gefunden werden.

+
+
+ ); + } + + return ( +
+
+
+
+

+ {post.title || "About"} +

+ {post.summary && ( +

+ {post.summary} +

+ )} +
+
+
+
+
+ ); +} diff --git a/src/app/admin/MonacoEditor.tsx b/src/app/admin/MonacoEditor.tsx new file mode 100644 index 0000000..f8c2137 --- /dev/null +++ b/src/app/admin/MonacoEditor.tsx @@ -0,0 +1,12 @@ +import Editor from "@monaco-editor/react"; + +export default function MonacoEditorWrapper(props: any) { + return ( + + ); +} \ No newline at end of file diff --git a/src/app/admin/editor/page.tsx b/src/app/admin/editor/page.tsx new file mode 100644 index 0000000..3baa12f --- /dev/null +++ b/src/app/admin/editor/page.tsx @@ -0,0 +1,621 @@ +"use client"; +import React, { useEffect, useRef, useState } from "react"; +import dynamic from "next/dynamic"; +import { useRouter } from "next/navigation"; +import "@fontsource/jetbrains-mono"; +import { marked } from "marked"; + +const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false }); + +// File/folder types from API +interface FileNode { + type: "post"; + slug: string; + title: string; + date: string; + tags: string[]; + summary: string; + content: string; + createdAt: string; + pinned: boolean; +} +interface FolderNode { + type: "folder"; + name: string; + path: string; + emoji: string; + children: (FileNode | FolderNode)[]; +} +type Node = FileNode | FolderNode; + +// Helper to strip YAML frontmatter +function stripFrontmatter(md: string): string { + if (!md) return ''; + if (md.startsWith('---')) { + const end = md.indexOf('---', 3); + if (end !== -1) return md.slice(end + 3).replace(/^\s+/, ''); + } + return md; +} + +// Helper to extract YAML frontmatter +function extractFrontmatter(md: string): { frontmatter: string; content: string } { + if (!md) return { frontmatter: '', content: '' }; + if (md.startsWith('---')) { + const end = md.indexOf('---', 3); + if (end !== -1) { + const frontmatter = md.slice(0, end + 3); + const content = md.slice(end + 3).replace(/^\s+/, ''); + return { frontmatter, content }; + } + } + return { frontmatter: '', content: md }; +} + +// Helper to combine frontmatter and content +function combineFrontmatterAndContent(frontmatter: string, content: string): string { + if (!frontmatter) return content; + if (!content) return frontmatter; + return frontmatter + '\n\n' + content; +} + +function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: { + nodes: Node[]; + onSelect: (slug: string) => void; + selectedSlug: string | null; + level?: number; +}) { + const [openFolders, setOpenFolders] = useState>({}); + return ( +
    + {nodes.map((node) => { + if (node.type === "folder") { + const isOpen = openFolders[node.path] ?? true; + return ( +
  • + + {isOpen && ( + + )} +
  • + ); + } else { + return ( +
  • + +
  • + ); + } + })} +
+ ); +} + +export default function EditorPage() { + // State + const router = useRouter(); + const [tree, setTree] = useState([]); + const [selectedSlug, setSelectedSlug] = useState(null); + const [fileContent, setFileContent] = useState(""); + const [originalContent, setOriginalContent] = useState(""); + const [fileTitle, setFileTitle] = useState(""); + const [vimMode, setVimMode] = useState(false); + const [previewHtml, setPreviewHtml] = useState(""); + const [split, setSplit] = useState(50); // percent - default to 50/50 split + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [browserOpen, setBrowserOpen] = useState(true); + const [showUnsavedDialog, setShowUnsavedDialog] = useState(false); + const [pendingNavigation, setPendingNavigation] = useState(null); + const editorRef = useRef(null); + const monacoVimRef = useRef(null); + + // Check if there are unsaved changes + const hasUnsavedChanges = fileContent !== originalContent; + + // Handle browser beforeunload event + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (hasUnsavedChanges) { + e.preventDefault(); + e.returnValue = ''; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [hasUnsavedChanges]); + + // Handle back navigation with unsaved changes check + const handleBackNavigation = () => { + if (hasUnsavedChanges) { + setShowUnsavedDialog(true); + setPendingNavigation('/admin'); + } else { + router.push('/admin'); + } + }; + + // Handle unsaved changes dialog actions + const handleUnsavedDialogAction = (action: 'save' | 'discard' | 'cancel') => { + if (action === 'save') { + handleSave().then(() => { + setOriginalContent(fileContent); // Reset unsaved state after save + setShowUnsavedDialog(false); + setPendingNavigation(null); + if (pendingNavigation) { + router.push(pendingNavigation); + } + }); + } else if (action === 'discard') { + setFileContent(originalContent); // Revert to last saved + setShowUnsavedDialog(false); + setPendingNavigation(null); + if (pendingNavigation) { + router.push(pendingNavigation); + } + } else { + setShowUnsavedDialog(false); + setPendingNavigation(null); + } + }; + + // Fetch file tree + useEffect(() => { + fetch("/api/posts") + .then(r => r.json()) + .then(setTree); + }, []); + + // Load file content when selected + useEffect(() => { + if (!selectedSlug) return; + setLoading(true); + fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`) + .then(r => r.json()) + .then(data => { + const { frontmatter, content } = extractFrontmatter(data.raw || data.content || ""); + const combinedContent = combineFrontmatterAndContent(frontmatter, content); + setFileContent(combinedContent); + setOriginalContent(combinedContent); + setFileTitle(data.title || data.slug || ""); + setLoading(false); + }); + }, [selectedSlug]); + + // Save file + async function handleSave() { + if (!selectedSlug) return; + setSaving(true); + + try { + // First save the file + const saveResponse = await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ markdown: fileContent }) + }); + + if (!saveResponse.ok) { + throw new Error('Failed to save file'); + } + + // Then call Rust backend to reparse this specific post + const reparseResponse = await fetch(`/api/admin/posts?reparsePost=${encodeURIComponent(selectedSlug)}`); + + if (!reparseResponse.ok) { + console.warn('Failed to reparse post, but file was saved'); + } else { + console.log('Post saved and reparsed successfully'); + } + setOriginalContent(fileContent); // Reset unsaved state after save + } catch (error) { + console.error('Error saving/reparsing post:', error); + } finally { + setSaving(false); + } + } + + // Live preview (JS markdown, not Rust) + useEffect(() => { + if (!fileContent) { setPreviewHtml(""); return; } + const { content } = extractFrontmatter(fileContent); + const html = typeof marked.parse === 'function' ? marked.parse(content) : ''; + if (typeof html === 'string') setPreviewHtml(html); + else if (html instanceof Promise) html.then(setPreviewHtml); + else setPreviewHtml(''); + }, [fileContent]); + + // Monaco Vim integration + async function handleEditorDidMount(editor: any, monaco: any) { + editorRef.current = editor; + + // Ensure editor resizes properly + const resizeObserver = new ResizeObserver(() => { + editor.layout(); + }); + + const editorContainer = editor.getContainerDomNode(); + if (editorContainer) { + resizeObserver.observe(editorContainer); + } + + if (vimMode) { + const { initVimMode } = await import("monaco-vim"); + if (monacoVimRef.current) monacoVimRef.current.dispose(); + monacoVimRef.current = initVimMode(editor, document.getElementById("vim-status")); + } + } + useEffect(() => { + if (!editorRef.current) return; + let disposed = false; + async function setupVim() { + if (monacoVimRef.current) monacoVimRef.current.dispose(); + if (vimMode) { + const { initVimMode } = await import("monaco-vim"); + if (!disposed) { + monacoVimRef.current = initVimMode(editorRef.current, document.getElementById("vim-status")); + } + } + } + setupVim(); + return () => { disposed = true; }; + }, [vimMode]); + + // Split drag logic + const dragRef = useRef(false); + const [isDragging, setIsDragging] = useState(false); + + function onDrag(e: React.MouseEvent | MouseEvent) { + if (!dragRef.current) return; + const percent = (e.clientX / window.innerWidth) * 100; + setSplit(percent); // No min/max limits + } + + function onDragStart() { + dragRef.current = true; + setIsDragging(true); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + } + + function onDragEnd() { + dragRef.current = false; + setIsDragging(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } + + useEffect(() => { + function onMove(e: MouseEvent) { + if (dragRef.current) { + onDrag(e); + } + } + + function onUp() { + if (dragRef.current) { + onDragEnd(); + } + } + + if (isDragging) { + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + } + }, [isDragging]); + + // Layout logic for left pane (file browser + editor) + const leftPaneWidth = `${split}%`; + const fileBrowserWidth = 240; + + // Only render MonacoEditor if the editor pane is visible and has width + const showEditor = true; // Always show editor, it will resize based on container + + return ( +
+ {/* Header */} +
+
+ {/* VS Code SVG Icon (smaller) */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Markdown Bearbeiter +
+
+ {/* Back Button */} + + + {/* Save Button */} + + + {/* Vim Mode Button */} + +
+
+ {/* Split Layout */} +
+ {/* Left: File browser + Editor */} +
+ {/* File Browser Collapsible Toggle */} +
+ +
+ {/* File browser content */} + {browserOpen && ( +
+
+ {tree.length === 0 ? ( +
Keine Datein gefunden.
+ ) : ( + + )} +
+
+ )} + {/* Monaco Editor */} +
+
+ {showEditor && ( + + setFileContent(v ?? "")} + onMount={handleEditorDidMount} + /> + + )} +
+
+
+
+ {/* Draggable Splitter - always show */} +
+ {/* Drag handle indicator */} +
+
+
+
+ {/* Right: Live Preview */} +
+
+
+

+ {fileTitle} +

+
+
+
+
+
+ + {/* Unsaved Changes Dialog */} + {showUnsavedDialog && ( +
+
+
+
+ + + +
+

Ungespeicherte Γ„nderungen

+
+

+ Sie haben ungespeicherte Γ„nderungen. MΓΆchten Sie diese speichern, bevor Sie fortfahren? +

+
+ + + +
+
+
+ )} +
+ ); +} + +// ErrorBoundary component for Monaco +class MonacoErrorBoundary extends React.Component<{children: React.ReactNode}, {error: Error | null}> { + constructor(props: any) { + super(props); + this.state = { error: null }; + } + static getDerivedStateFromError(error: Error) { + return { error }; + } + componentDidCatch(error: Error, info: any) { + // Optionally log error + } + render() { + if (this.state.error) { + return
Fehler: {this.state.error.message}
; + } + return this.props.children; + } +} \ No newline at end of file diff --git a/src/app/admin/manage/rust-status/page.tsx b/src/app/admin/manage/rust-status/page.tsx index a09982a..ecb8dfc 100644 --- a/src/app/admin/manage/rust-status/page.tsx +++ b/src/app/admin/manage/rust-status/page.tsx @@ -103,6 +103,19 @@ export default function RustStatusPage() { } }; + const reinterpretAllPosts = async () => { + try { + const res = await fetch('/api/admin/posts?reinterpretAll=1'); + if (!res.ok) throw new Error('Fehler beim Neuinterpretieren der Posts'); + const data = await res.json(); + console.log('Reinterpret result:', data); + // Refresh all data after reinterpret + await Promise.all([fetchStats(), fetchHealth(), fetchLogs()]); + } catch (e: any) { + console.error('Error reinterpreting posts:', e); + } + }; + useEffect(() => { fetchStats(); fetchHealth(); @@ -358,6 +371,13 @@ export default function RustStatusPage() {

Parser Logs

+