Merge pull request 'feature/rust-healthcheck-and-frontend' (#10) from feature/rust-healthcheck-and-frontend into main
Some checks failed
Deploy / build-and-deploy (push) Failing after 2s

Reviewed-on: http://10.0.0.13:3002/rattatwinko/markdownblog/pulls/10

fine merge
rt - 26jun25
This commit is contained in:
2025-06-28 18:48:18 +00:00
7 changed files with 353 additions and 7 deletions

View File

@@ -1,8 +1,12 @@
#[warn(unused_imports)]
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
mod markdown; mod markdown;
use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts}; use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts};
use serde_json; use serde_json;
use std::fs; use std::fs;
use std::io;
use std::io::Read; // STD AYOOOOOOOOOOOOOO - Tsodin
#[derive(Parser)] #[derive(Parser)]
#[command(name = "Markdown Backend")] #[command(name = "Markdown Backend")]
@@ -28,6 +32,17 @@ enum Commands {
Watch, Watch,
/// Show Rust parser statistics /// Show Rust parser statistics
Rsparseinfo, Rsparseinfo,
/// Check backend health
Checkhealth,
/// Parse markdown from file or stdin
Parse {
#[arg(long)]
file: Option<String>,
#[arg(long)]
stdin: bool,
#[arg(long)]
ast: bool,
},
} }
fn main() { fn main() {
@@ -73,5 +88,43 @@ fn main() {
Commands::Rsparseinfo => { Commands::Rsparseinfo => {
println!("{}", markdown::rsparseinfo()); println!("{}", markdown::rsparseinfo());
} }
Commands::Checkhealth => {
let health = markdown::checkhealth();
println!("{}", serde_json::to_string_pretty(&health).unwrap());
}
Commands::Parse { file, stdin, ast } => {
let input = if let Some(file_path) = file {
match std::fs::read_to_string(file_path) {
Ok(content) => content,
Err(e) => {
eprintln!("Failed to read file: {}", e);
std::process::exit(1);
}
}
} else if *stdin {
let mut buffer = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buffer) {
eprintln!("Failed to read from stdin: {}", e);
std::process::exit(1);
}
buffer
} else {
eprintln!("Please provide --file <path> or --stdin");
std::process::exit(1);
};
if *ast {
// Print pulldown_cmark events as debug output
let parser = pulldown_cmark::Parser::new_ext(&input, pulldown_cmark::Options::all());
for event in parser {
println!("{:?}", event);
}
} else {
// Print HTML output
let parser = pulldown_cmark::Parser::new_ext(&input, pulldown_cmark::Options::all());
let mut html_output = String::new();
pulldown_cmark::html::push_html(&mut html_output, parser);
println!("{}", html_output);
}
}
} }
} }

View File

@@ -8,7 +8,7 @@ BLAZINGLY FAST!
*/ */
#[warn(unused_imports)]
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@@ -29,6 +29,8 @@ use std::collections::HashMap;
use std::sync::RwLock; use std::sync::RwLock;
use serde_json; use serde_json;
use sysinfo::{System, Pid, RefreshKind, CpuRefreshKind, ProcessRefreshKind}; use sysinfo::{System, Pid, RefreshKind, CpuRefreshKind, ProcessRefreshKind};
use serde::Serialize;
use regex::Regex;
const POSTS_CACHE_PATH: &str = "./cache/posts_cache.json"; const POSTS_CACHE_PATH: &str = "./cache/posts_cache.json";
const POST_STATS_PATH: &str = "./cache/post_stats.json"; const POST_STATS_PATH: &str = "./cache/post_stats.json";
@@ -68,6 +70,19 @@ static POST_CACHE: Lazy<RwLock<HashMap<String, Post>>> = Lazy::new(|| RwLock::ne
static ALL_POSTS_CACHE: Lazy<RwLock<Option<Vec<Post>>>> = Lazy::new(|| RwLock::new(None)); static ALL_POSTS_CACHE: Lazy<RwLock<Option<Vec<Post>>>> = Lazy::new(|| RwLock::new(None));
static POST_STATS: Lazy<RwLock<HashMap<String, PostStats>>> = Lazy::new(|| RwLock::new(HashMap::new())); static POST_STATS: Lazy<RwLock<HashMap<String, PostStats>>> = Lazy::new(|| RwLock::new(HashMap::new()));
#[derive(Debug, Serialize)]
pub struct HealthReport {
pub posts_dir_exists: bool,
pub posts_count: usize,
pub cache_file_exists: bool,
pub cache_stats_file_exists: bool,
pub cache_readable: bool,
pub cache_stats_readable: bool,
pub cache_post_count: Option<usize>,
pub cache_stats_count: Option<usize>,
pub errors: Vec<String>,
}
fn get_posts_directory() -> PathBuf { fn get_posts_directory() -> PathBuf {
let candidates = [ let candidates = [
"./posts", "./posts",
@@ -131,6 +146,51 @@ fn strip_emojis(s: &str) -> String {
.collect() .collect()
} }
// Function to process custom tags in markdown content
fn process_custom_tags(content: &str) -> String {
let mut processed = content.to_string();
// Handle simple tags without parameters FIRST
let simple_tags = [
("<mytag />", "<div class=\"custom-tag mytag\">This is my custom tag content!</div>"),
("<warning />", "<div class=\"custom-tag warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warning: This is a custom warning tag!</div>"),
("<info />", "<div class=\"custom-tag info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\"> Info: This is a custom info tag!</div>"),
("<success />", "<div class=\"custom-tag success\" style=\"background: #d4edda; border: 1px solid #c3e6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">✅ Success: This is a custom success tag!</div>"),
("<error />", "<div class=\"custom-tag error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Error: This is a custom error tag!</div>"),
];
for (tag, replacement) in simple_tags.iter() {
processed = processed.replace(tag, replacement);
}
// Handle tags with parameters like <mytag param="value" />
let tag_with_params = Regex::new(r"<(\w+)\s+([^>]*?[a-zA-Z0-9=])[^>]*/>").unwrap();
processed = tag_with_params.replace_all(&processed, |caps: &regex::Captures| {
let tag_name = &caps[1];
let params = &caps[2];
match tag_name {
"mytag" => {
// Parse parameters and generate custom HTML
format!("<div class=\"custom-tag mytag\" data-params=\"{}\">Custom content with params: {}</div>", params, params)
},
"alert" => {
// Parse alert type from params
if params.contains("type=\"warning\"") {
"<div class=\"custom-tag alert warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warning Alert!</div>".to_string()
} else if params.contains("type=\"error\"") {
"<div class=\"custom-tag alert error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Error Alert!</div>".to_string()
} else {
"<div class=\"custom-tag alert info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\"> Info Alert!</div>".to_string()
}
},
_ => format!("<div class=\"custom-tag {}\">Unknown custom tag: {}</div>", tag_name, tag_name)
}
}).to_string();
processed
}
static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| { static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
let mut builder = ammonia::Builder::default(); let mut builder = ammonia::Builder::default();
// All possible HTML Tags so that you can stylize via HTML // All possible HTML Tags so that you can stylize via HTML
@@ -156,7 +216,7 @@ static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
builder.add_tag_attributes("pre", &["style"]); builder.add_tag_attributes("pre", &["style"]);
builder.add_tag_attributes("kbd", &["style"]); builder.add_tag_attributes("kbd", &["style"]);
builder.add_tag_attributes("samp", &["style"]); builder.add_tag_attributes("samp", &["style"]);
builder.add_tag_attributes("div", &["style"]); builder.add_tag_attributes("div", &["style", "class"]);
builder.add_tag_attributes("section", &["style"]); builder.add_tag_attributes("section", &["style"]);
builder.add_tag_attributes("article", &["style"]); builder.add_tag_attributes("article", &["style"]);
builder.add_tag_attributes("header", &["style"]); builder.add_tag_attributes("header", &["style"]);
@@ -266,7 +326,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
let created_at = get_file_creation_date(&file_path)?; let created_at = get_file_creation_date(&file_path)?;
let processed_markdown = process_anchor_links(&result.content); let processed_markdown = process_custom_tags(&process_anchor_links(&result.content));
let parser = Parser::new_ext(&processed_markdown, Options::all()); let parser = Parser::new_ext(&processed_markdown, Options::all());
let mut html_output = String::new(); let mut html_output = String::new();
let mut heading_text = String::new(); let mut heading_text = String::new();
@@ -439,4 +499,66 @@ pub fn save_post_cache_to_disk() {
let _ = fs::create_dir_all("./cache"); let _ = fs::create_dir_all("./cache");
let _ = fs::write(POST_STATS_PATH, map); let _ = fs::write(POST_STATS_PATH, map);
} }
}
pub fn checkhealth() -> HealthReport {
let mut errors = Vec::new();
let posts_dir = get_posts_directory();
let posts_dir_exists = posts_dir.exists() && posts_dir.is_dir();
let mut posts_count = 0;
if posts_dir_exists {
match std::fs::read_dir(&posts_dir) {
Ok(entries) => {
posts_count = entries.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|ext| ext == "md").unwrap_or(false))
.count();
},
Err(e) => errors.push(format!("Failed to read posts dir: {}", e)),
}
} else {
errors.push("Posts directory does not exist".to_string());
}
let cache_file_exists = Path::new(POSTS_CACHE_PATH).exists();
let cache_stats_file_exists = Path::new(POST_STATS_PATH).exists();
let (mut cache_readable, mut cache_post_count) = (false, None);
if cache_file_exists {
match std::fs::read_to_string(POSTS_CACHE_PATH) {
Ok(data) => {
match serde_json::from_str::<HashMap<String, Post>>(&data) {
Ok(map) => {
cache_readable = true;
cache_post_count = Some(map.len());
},
Err(e) => errors.push(format!("Cache file not valid JSON: {}", e)),
}
},
Err(e) => errors.push(format!("Failed to read cache file: {}", e)),
}
}
let (mut cache_stats_readable, mut cache_stats_count) = (false, None);
if cache_stats_file_exists {
match std::fs::read_to_string(POST_STATS_PATH) {
Ok(data) => {
match serde_json::from_str::<HashMap<String, PostStats>>(&data) {
Ok(map) => {
cache_stats_readable = true;
cache_stats_count = Some(map.len());
},
Err(e) => errors.push(format!("Cache stats file not valid JSON: {}", e)),
}
},
Err(e) => errors.push(format!("Failed to read cache stats file: {}", e)),
}
}
HealthReport {
posts_dir_exists,
posts_count,
cache_file_exists,
cache_stats_file_exists,
cache_readable,
cache_stats_readable,
cache_post_count,
cache_stats_count,
errors,
}
} }

View File

@@ -24,6 +24,7 @@ author: Rattatwinko
- [Features 🎉](#features) - [Features 🎉](#features)
- [Administration 🚧](#administration) - [Administration 🚧](#administration)
- [Customization 🎨](#customization) - [Customization 🎨](#customization)
- [Creating Posts with MdB ✍](#creating-posts-with-mdb)
- [Troubleshooting 🚨](#troubleshooting) - [Troubleshooting 🚨](#troubleshooting)
- [Support 🤝](#support) - [Support 🤝](#support)
- [Support the Project ❤️](#support-the-project) - [Support the Project ❤️](#support-the-project)
@@ -31,7 +32,7 @@ author: Rattatwinko
- [Folder Emojis 🇦🇹](#folder-emoji-technical-note) - [Folder Emojis 🇦🇹](#folder-emoji-technical-note)
- [API 🏗️](#api) - [API 🏗️](#api)
- [ToT, and Todo](#train-of-thought-for-this-project-and-todo) - [ToT, and Todo](#train-of-thought-for-this-project-and-todo)
- [Recent Changes](#) - [Recent Changes](#recent-changes)
--- ---
@@ -302,6 +303,35 @@ The codebase is well-structured and documented. Key files:
--- ---
## Creating Posts with MdB
If you are reading posts. Then you probably dont need this explenation!
Else you should read this.
First of all, if you are creating posts within the terminal. then you should create posts with the following headers.
```Markdown
---
title: Welcome to MarkdownBlog
date: '2025-06-19'
tags:
- welcome
- introduction
- getting-started
- documentation
summary: A comprehensive guide to getting started with MarkdownBlog
author: Rattatwinko
---
```
As you can see this is the header for the current Post.
You can write this like YML (idk).
If you are writing posts within the Admin-Panel then you are a _lucky piece of shit_ cause there it does that **automatically**
---
## Troubleshooting ## Troubleshooting
### Common Issues ### Common Issues
@@ -450,4 +480,4 @@ If you are wondering:
> >
> *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer > *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer
> >
> <cite>— Rattatwinko, 2025 Q3</cite> > <cite>— Rattatwinko, 2025 Q3</cite>

22
run-local-backend.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# This script builds and runs the Rust backend locally, similar to the Docker container.
# Usage: ./run-local-backend.sh [args for markdown_backend]
# AnalSex with the frontend ( Cursor Autocompletion xD )
set -e
# Set environment variables as in Docker (customize as needed)
export BLOG_OWNER=${BLOG_OWNER:-"rattatwinko"}
# Build the backend in release mode
cd "$(dirname "$0")/markdown_backend"
echo "Building Rust backend..."
cargo build --release
# Run the backend with any arguments passed to the script
cd target/release
echo "Running: ./markdown_backend $@"
./markdown_backend "$@"
npm run dev ## start the fuckass frontend

View File

@@ -9,10 +9,25 @@ interface PostStats {
last_compile_time_ms: number; last_compile_time_ms: number;
} }
interface HealthReport {
posts_dir_exists: boolean;
posts_count: number;
cache_file_exists: boolean;
cache_stats_file_exists: boolean;
cache_readable: boolean;
cache_stats_readable: boolean;
cache_post_count?: number;
cache_stats_count?: number;
errors: string[];
}
export default function RustStatusPage() { export default function RustStatusPage() {
const [stats, setStats] = useState<PostStats[]>([]); const [stats, setStats] = useState<PostStats[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [health, setHealth] = useState<HealthReport | null>(null);
const [healthLoading, setHealthLoading] = useState(true);
const [healthError, setHealthError] = useState<string | null>(null);
// Summary calculations // Summary calculations
const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0); const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0);
@@ -35,8 +50,24 @@ export default function RustStatusPage() {
} }
}; };
const fetchHealth = async () => {
setHealthLoading(true);
setHealthError(null);
try {
const res = await fetch('/api/admin/posts?checkhealth=1');
if (!res.ok) throw new Error('Fehler beim Laden des Health-Checks');
const data = await res.json();
setHealth(data);
} catch (e: any) {
setHealthError(e.message || 'Unbekannter Fehler');
} finally {
setHealthLoading(false);
}
};
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
fetchHealth();
}, []); }, []);
return ( return (
@@ -88,7 +119,63 @@ export default function RustStatusPage() {
</div> </div>
</div> </div>
{/* Rest of your component remains the same */} {/* Health Check Section */}
<div className="mb-6">
<h2 className="text-base sm:text-lg font-semibold mb-2 text-center">Health-Check</h2>
{healthLoading && <div className="text-center py-4 text-base">Lade Health-Check...</div>}
{healthError && <div className="text-red-500 text-center text-base">{healthError}</div>}
{health && (
<div className="bg-white rounded-lg shadow p-4 flex flex-col gap-2 items-center">
<div className="flex flex-wrap gap-4 justify-center">
<div className="flex flex-col items-center">
<span className={`text-lg font-bold ${health.posts_dir_exists ? 'text-green-700' : 'text-red-700'}`}>{health.posts_dir_exists ? '✔' : '✖'}</span>
<span className="text-xs text-gray-600">Posts-Verzeichnis</span>
</div>
<div className="flex flex-col items-center">
<span className="text-lg font-bold text-blue-700">{health.posts_count}</span>
<span className="text-xs text-gray-600">Posts</span>
</div>
<div className="flex flex-col items-center">
<span className={`text-lg font-bold ${health.cache_file_exists ? 'text-green-700' : 'text-red-700'}`}>{health.cache_file_exists ? '✔' : '✖'}</span>
<span className="text-xs text-gray-600">Cache-Datei</span>
</div>
<div className="flex flex-col items-center">
<span className={`text-lg font-bold ${health.cache_stats_file_exists ? 'text-green-700' : 'text-red-700'}`}>{health.cache_stats_file_exists ? '✔' : '✖'}</span>
<span className="text-xs text-gray-600">Cache-Stats</span>
</div>
<div className="flex flex-col items-center">
<span className={`text-lg font-bold ${health.cache_readable ? 'text-green-700' : 'text-red-700'}`}>{health.cache_readable ? '✔' : '✖'}</span>
<span className="text-xs text-gray-600">Cache lesbar</span>
</div>
<div className="flex flex-col items-center">
<span className={`text-lg font-bold ${health.cache_stats_readable ? 'text-green-700' : 'text-red-700'}`}>{health.cache_stats_readable ? '✔' : '✖'}</span>
<span className="text-xs text-gray-600">Stats lesbar</span>
</div>
{typeof health.cache_post_count === 'number' && (
<div className="flex flex-col items-center">
<span className="text-lg font-bold text-blue-700">{health.cache_post_count}</span>
<span className="text-xs text-gray-600">Cache-Posts</span>
</div>
)}
{typeof health.cache_stats_count === 'number' && (
<div className="flex flex-col items-center">
<span className="text-lg font-bold text-blue-700">{health.cache_stats_count}</span>
<span className="text-xs text-gray-600">Stats-Einträge</span>
</div>
)}
</div>
{health.errors.length > 0 && (
<div className="mt-2 text-red-600 text-xs text-center">
<b>Fehler:</b>
<ul className="list-disc ml-5 inline-block text-left">
{health.errors.map((err, i) => <li key={i}>{err}</li>)}
</ul>
</div>
)}
</div>
)}
</div>
{/* Summary Cards */} {/* Summary Cards */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-green-100 rounded-lg p-4 flex flex-col items-center shadow"> <div className="bg-green-100 rounded-lg p-4 flex flex-col items-center shadow">

View File

@@ -70,6 +70,26 @@ export async function GET(request: Request) {
}); });
} }
} }
const checkhealth = searchParams.get('checkhealth');
if (checkhealth === '1') {
// Call the Rust backend for health check
const rustResult = spawnSync(
process.cwd() + '/markdown_backend/target/release/markdown_backend',
['checkhealth'],
{ encoding: 'utf-8' }
);
if (rustResult.status === 0 && rustResult.stdout) {
return new Response(rustResult.stdout, {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} else {
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
// Return the current pinned.json object // Return the current pinned.json object
try { try {
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json'); const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');

View File

@@ -443,4 +443,16 @@ select:focus {
.prose a { .prose a {
word-break: break-word; word-break: break-word;
} }
} }
.custom-tag {
padding: 1rem;
border-radius: 6px;
margin: 1rem 0;
font-weight: 500;
}
.custom-tag.warning { background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; }
.custom-tag.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
.custom-tag.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.custom-tag.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
.custom-tag.mytag { background: #e3e3ff; border: 1px solid #b3b3ff; color: #333366; }