diff --git a/markdown_backend/src/main.rs b/markdown_backend/src/main.rs index 8dc14fc..176d972 100644 --- a/markdown_backend/src/main.rs +++ b/markdown_backend/src/main.rs @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand}; mod markdown; use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts}; use serde_json; +use std::fs; #[derive(Parser)] #[command(name = "Markdown Backend")] @@ -30,6 +31,7 @@ enum Commands { } fn main() { + markdown::load_post_cache_from_disk(); let cli = Cli::parse(); match &cli.command { Commands::List => { @@ -43,6 +45,7 @@ fn main() { match get_post_by_slug(slug) { Ok(post) => { println!("{}", serde_json::to_string(&post).unwrap()); + markdown::save_post_cache_to_disk(); } Err(e) => { eprintln!("{}", e); diff --git a/markdown_backend/src/markdown.rs b/markdown_backend/src/markdown.rs index fbfe6f6..db40889 100644 --- a/markdown_backend/src/markdown.rs +++ b/markdown_backend/src/markdown.rs @@ -30,6 +30,9 @@ use std::sync::RwLock; use serde_json; use sysinfo::{System, Pid, RefreshKind, CpuRefreshKind, ProcessRefreshKind}; +const POSTS_CACHE_PATH: &str = "./cache/posts_cache.json"; +const POST_STATS_PATH: &str = "./cache/post_stats.json"; + #[derive(Debug, Deserialize, Clone, serde::Serialize)] pub struct PostFrontmatter { pub title: String, @@ -38,7 +41,7 @@ pub struct PostFrontmatter { pub summary: Option, } -#[derive(Debug, Clone, serde::Serialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Post { pub slug: String, pub title: String, @@ -50,7 +53,7 @@ pub struct Post { pub author: String, } -#[derive(Debug, Clone, serde::Serialize, Default)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] pub struct PostStats { pub slug: String, pub cache_hits: u64, @@ -306,6 +309,8 @@ pub fn get_all_posts() -> Result, Box> { if path.extension().map(|e| e == "md").unwrap_or(false) { let file_stem = path.file_stem().unwrap().to_string_lossy(); if let Ok(post) = get_post_by_slug(&file_stem) { + // Insert each post into the individual post cache as well + POST_CACHE.write().unwrap().insert(file_stem.to_string(), post.clone()); posts.push(post); } } @@ -342,4 +347,28 @@ pub fn watch_posts(on_change: F) -> notify::Result>(&data) { + *POST_CACHE.write().unwrap() = map; + } + } + if let Ok(data) = fs::read_to_string(POST_STATS_PATH) { + if let Ok(map) = serde_json::from_str::>(&data) { + *POST_STATS.write().unwrap() = map; + } + } +} + +pub fn save_post_cache_to_disk() { + 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); + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 47f0e54..44edc60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "autoprefixer": "^10.4.17", "bcrypt": "^5.0.2", "bcryptjs": "^2.4.3", + "chart.js": "^4.5.0", "chokidar": "^3.6.0", "date-fns": "^3.6.0", "dompurify": "^3.0.9", @@ -28,6 +29,7 @@ "pm2": "^6.0.8", "postcss": "^8.4.35", "react": "^18.2.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.2.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3" @@ -502,6 +504,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -2214,6 +2222,18 @@ "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==", "license": "MIT/X11" }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6805,6 +6825,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/package.json b/package.json index bd66501..7893274 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "autoprefixer": "^10.4.17", "bcrypt": "^5.0.2", "bcryptjs": "^2.4.3", + "chart.js": "^4.5.0", "chokidar": "^3.6.0", "date-fns": "^3.6.0", "dompurify": "^3.0.9", @@ -31,6 +32,7 @@ "pm2": "^6.0.8", "postcss": "^8.4.35", "react": "^18.2.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.2.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3" diff --git a/src/app/admin/manage/rust-status/page.tsx b/src/app/admin/manage/rust-status/page.tsx index 708316d..8a80514 100644 --- a/src/app/admin/manage/rust-status/page.tsx +++ b/src/app/admin/manage/rust-status/page.tsx @@ -1,5 +1,18 @@ 'use client'; import React, { useEffect, useState } from 'react'; +import { Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + ChartOptions, +} from 'chart.js'; + +ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); interface PostStats { slug: string; @@ -13,6 +26,8 @@ export default function RustStatusPage() { const [stats, setStats] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [autoRefresh, setAutoRefresh] = useState(false); + const autoRefreshRef = React.useRef(null); const fetchStats = async () => { setLoading(true); @@ -31,45 +46,156 @@ export default function RustStatusPage() { React.useEffect(() => { fetchStats(); - const interval = setInterval(fetchStats, 5000); - return () => clearInterval(interval); + // Listen for post changes via BroadcastChannel + let bc: BroadcastChannel | null = null; + if (typeof window !== 'undefined' && 'BroadcastChannel' in window) { + bc = new BroadcastChannel('posts-changed'); + bc.onmessage = (event) => { + if (event.data === 'changed') { + fetchStats(); + } + }; + } + return () => { + if (bc) bc.close(); + if (autoRefreshRef.current) clearInterval(autoRefreshRef.current); + }; }, []); + // Handle auto-refresh toggle + React.useEffect(() => { + if (autoRefresh) { + autoRefreshRef.current = setInterval(fetchStats, 2000); + } else if (autoRefreshRef.current) { + clearInterval(autoRefreshRef.current); + autoRefreshRef.current = null; + } + return () => { + if (autoRefreshRef.current) clearInterval(autoRefreshRef.current); + }; + }, [autoRefresh]); + + // Dashboard summary calculations + const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0); + const totalMisses = stats.reduce((sum, s) => sum + s.cache_misses, 0); + const avgInterpret = stats.length ? (stats.reduce((sum, s) => sum + s.last_interpret_time_ms, 0) / stats.length).toFixed(1) : 0; + const avgCompile = stats.length ? (stats.reduce((sum, s) => sum + s.last_compile_time_ms, 0) / stats.length).toFixed(1) : 0; + + // Chart data + const chartData = { + labels: stats.map(s => s.slug), + datasets: [ + { + label: 'Cache Hits', + data: stats.map(s => s.cache_hits), + backgroundColor: 'rgba(34,197,94,0.7)', + }, + { + label: 'Cache Misses', + data: stats.map(s => s.cache_misses), + backgroundColor: 'rgba(239,68,68,0.7)', + }, + ], + }; + const chartOptions: ChartOptions<'bar'> = { + responsive: true, + plugins: { + legend: { position: 'top' }, + title: { display: true, text: 'Cache Hits & Misses per Post' }, + }, + scales: { + x: { stacked: true }, + y: { stacked: true, beginAtZero: true }, + }, + }; + return ( -
-

Rust Parser Status

- {loading &&
Loading...
} - {error &&
{error}
} - {!loading && !error && ( -
- - - - - - - - - - - - {stats.length === 0 ? ( - - ) : ( - stats.map(stat => ( - - - - - - - - )) - )} - -
SlugCache HitsCache MissesLast Interpret Time (ms)Last Compile Time (ms)
No stats available.
{stat.slug}{stat.cache_hits}{stat.cache_misses}{stat.last_interpret_time_ms}{stat.last_compile_time_ms}
+
+

Rust Parser Dashboard

+
+ + +
+ {loading && ( +
+
+
Loading stats...
)} + {error && ( +
{error}
+ )} + {!loading && !error && ( + <> + {/* Summary Cards */} +
+
+ {totalHits} + Total Cache Hits +
+
+ {totalMisses} + Total Cache Misses +
+
+ {avgInterpret} ms + Avg Interpret Time +
+
+ {avgCompile} ms + Avg Compile Time +
+
+ + {/* Bar Chart */} +
+ +
+ + {/* Raw Data Table */} +
+ + + + + + + + + + + + {stats.length === 0 ? ( + + ) : ( + stats.map(stat => ( + + + + + + + + )) + )} + +
SlugCache HitsCache MissesLast Interpret Time (ms)Last Compile Time (ms)
No stats available.
{stat.slug}{stat.cache_hits}{stat.cache_misses}{stat.last_interpret_time_ms}{stat.last_compile_time_ms}
+
+ + )}
); } \ No newline at end of file