Enhance blog features and improve backend functionality

- Added a VS Code-style editor with YAML frontmatter support and live preview.
- Implemented force reparse functionality for immediate updates of posts.
- Improved directory scanning with error handling and automatic directory creation.
- Introduced new CLI commands for cache management: `reinterpret-all` and `reparse-post`.
- Enhanced logging for better debugging and monitoring of the Rust backend.
- Updated README to reflect new features and improvements.
This commit is contained in:
2025-07-05 22:23:58 +02:00
parent f94ddaa3b1
commit 21f13ef8ae
8 changed files with 705 additions and 110 deletions

View File

@@ -1,6 +1,7 @@
"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";
@@ -37,6 +38,27 @@ function stripFrontmatter(md: string): string {
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;
@@ -87,19 +109,73 @@ function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: {
export default function EditorPage() {
// State
const router = useRouter();
const [tree, setTree] = useState<Node[]>([]);
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
const [fileContent, setFileContent] = useState<string>("");
const [originalContent, setOriginalContent] = useState<string>("");
const [fileTitle, setFileTitle] = useState<string>("");
const [vimMode, setVimMode] = useState(false);
const [previewHtml, setPreviewHtml] = useState<string>("");
const [split, setSplit] = useState(50); // percent
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<string | null>(null);
const editorRef = useRef<any>(null);
const monacoVimRef = useRef<any>(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")
@@ -114,7 +190,10 @@ export default function EditorPage() {
fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`)
.then(r => r.json())
.then(data => {
setFileContent(stripFrontmatter(data.raw || data.content || ""));
const { frontmatter, content } = extractFrontmatter(data.raw || data.content || "");
const combinedContent = combineFrontmatterAndContent(frontmatter, content);
setFileContent(combinedContent);
setOriginalContent(combinedContent);
setFileTitle(data.title || data.slug || "");
setLoading(false);
});
@@ -124,18 +203,40 @@ export default function EditorPage() {
async function handleSave() {
if (!selectedSlug) return;
setSaving(true);
await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ markdown: fileContent })
});
setSaving(false);
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 html = typeof marked.parse === 'function' ? marked.parse(stripFrontmatter(fileContent)) : '';
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('');
@@ -144,6 +245,17 @@ export default function EditorPage() {
// 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();
@@ -168,25 +280,57 @@ export default function EditorPage() {
// Split drag logic
const dragRef = useRef(false);
function onDrag(e: React.MouseEvent) {
const [isDragging, setIsDragging] = useState(false);
function onDrag(e: React.MouseEvent | MouseEvent) {
if (!dragRef.current) return;
const percent = (e.clientX / window.innerWidth) * 100;
setSplit(Math.max(20, Math.min(80, percent)));
setSplit(percent); // No min/max limits
}
function onDragStart() { dragRef.current = true; document.body.style.cursor = "col-resize"; }
function onDragEnd() { dragRef.current = false; document.body.style.cursor = ""; }
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) { onDrag(e as any); }
function onUp() { onDragEnd(); }
if (dragRef.current) {
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); };
return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
};
}
}, [dragRef.current]);
}, [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 = browserOpen ? true : split > 5;
const showEditor = true; // Always show editor, it will resize based on container
return (
<div className="h-screen w-screen bg-white flex flex-col font-mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}>
@@ -241,50 +385,101 @@ export default function EditorPage() {
</linearGradient>
</defs>
</svg>
<span className="text-black text-lg font-semibold">Markdown Editor</span>
<span className="text-black text-lg font-semibold">Markdown Bearbeiter</span>
</div>
<div className="flex gap-2 items-center">
{/* Back Button */}
<button
onClick={handleSave}
className={`flex items-center gap-2 px-3 py-1 rounded transition-colors border border-blue-400 bg-blue-500 text-white hover:bg-blue-600 font-mono ${saving ? 'opacity-60 cursor-wait' : ''}`}
onClick={() => handleBackNavigation()}
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border font-mono text-sm sm:text-base ${
saving
? 'opacity-60 cursor-wait border-red-400 bg-red-500 text-white'
: hasUnsavedChanges
? 'border-orange-400 bg-orange-500 text-white hover:bg-orange-600'
: 'border-red-400 bg-red-500 text-white hover:bg-red-600'
}`}
disabled={saving}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1" /></svg>
<span>Save</span>
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
<span className="hidden sm:inline">
{hasUnsavedChanges ? 'Zurück*' : 'Zurück'}
</span>
</button>
{/* Save Button */}
<button
onClick={handleSave}
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border font-mono text-sm sm:text-base ${
saving
? 'opacity-60 cursor-wait border-blue-400 bg-blue-500 text-white'
: hasUnsavedChanges
? 'border-orange-400 bg-orange-500 text-white hover:bg-orange-600'
: 'border-blue-400 bg-blue-500 text-white hover:bg-blue-600'
}`}
disabled={saving}
>
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1" /></svg>
<span className="hidden sm:inline">
{saving
? 'Am Speichern...'
: hasUnsavedChanges
? 'Speichern*'
: 'Speichern'
}
</span>
</button>
{/* Vim Mode Button */}
<button
onClick={() => setVimMode((v) => !v)}
className={`flex items-center gap-2 px-3 py-1 rounded transition-colors border border-gray-300 ${vimMode ? "bg-green-600 text-white" : "bg-white text-gray-700 hover:bg-gray-100"}`}
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border border-gray-300 text-sm sm:text-base ${
vimMode
? "bg-green-600 text-white"
: "bg-white text-gray-700 hover:bg-gray-100"
}`}
>
{/* Actual Vim SVG Icon */}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-5 h-5" fill="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor">
<title>vim</title>
<path d="M26.445 22.095l0.592-0.649h1.667l0.386 0.519-1.581 5.132h0.616l-0.1 0.261h-2.228l1.405-4.454h-2.518l-1.346 4.238h0.53l-0.091 0.217h-2.006l1.383-4.434h-2.619l-1.327 4.172h0.545l-0.090 0.261h-2.076l1.892-5.573h-0.732l0.114-0.339h2.062l0.649 0.671h1.132l0.614-0.692h1.326l0.611 0.669zM7.99 27.033h-2.141l-0.327-0.187v-21.979h-1.545l-0.125-0.125v-1.47l0.179-0.192h9.211l0.266 0.267v1.385l-0.177 0.216h-1.348v10.857l11.006-10.857h-2.607l-0.219-0.235v-1.453l0.151-0.139h9.36l0.165 0.166v1.337l-12.615 12.937h-0.466c-0.005-0-0.011-0-0.018-0-0.012 0-0.024 0.001-0.036 0.002l0.002-0-0.025 0.004c-0.058 0.012-0.108 0.039-0.149 0.075l0-0-0.429 0.369-0.005 0.004c-0.040 0.037-0.072 0.084-0.090 0.136l-0.001 0.002-0.37 1.037zM17.916 18.028l0.187 0.189-0.336 1.152-0.281 0.282h-1.211l-0.226-0.226 0.389-1.088 0.36-0.309zM13.298 27.42l1.973-5.635h-0.626l0.371-0.38h2.073l-1.953 5.692h0.779l-0.099 0.322zM30.996 15.982h-0.034l-5.396-5.396 5.377-5.516v-2.24l-0.811-0.81h-10.245l-0.825 0.756v1.306l-3.044-3.044v-0.034l-0.019 0.018-0.018-0.018v0.034l-1.612 1.613-0.672-0.673h-10.151l-0.797 0.865v2.356l0.77 0.77h0.9v6.636l-3.382 3.38h-0.034l0.018 0.016-0.018 0.017h0.034l3.382 3.382v8.081l1.133 0.654h2.902l2.321-2.379 5.206 5.206v0.035l0.019-0.017 0.017 0.017v-0.035l3.136-3.135h0.606c0.144-0.001 0.266-0.093 0.312-0.221l0.001-0.002 0.182-0.532c0.011-0.031 0.017-0.067 0.017-0.105 0-0.073-0.024-0.14-0.064-0.195l0.001 0.001 1.827-1.827-0.765 2.452c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.275 0.233 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.097 0.015 0 0 0 0 0 0h2.279c0.136-0.001 0.252-0.084 0.303-0.201l0.001-0.002 0.206-0.492c0.014-0.036 0.022-0.077 0.022-0.121 0-0.048-0.010-0.094-0.028-0.135l0.001 0.002c-0.035-0.082-0.1-0.145-0.18-0.177l-0.002-0.001c-0.036-0.015-0.077-0.024-0.121-0.025h-0.094l1.050-3.304h1.54l-1.27 4.025c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.274 0.232 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.098 0.015 0 0 0.001 0 0.001 0h2.502c0 0 0.001 0 0.001 0 0.14 0 0.26-0.087 0.308-0.21l0.001-0.002 0.205-0.535c0.013-0.034 0.020-0.073 0.020-0.114 0-0.142-0.090-0.264-0.215-0.311l-0.002-0.001c-0.034-0.013-0.073-0.021-0.114-0.021h-0.181l1.413-4.59c0.011-0.031 0.017-0.066 0.017-0.103 0-0.074-0.025-0.143-0.066-0.198l0.001 0.001-0.469-0.63-0.004-0.006c-0.061-0.078-0.156-0.127-0.261-0.127h-1.795c-0.093 0-0.177 0.039-0.237 0.101l-0 0-0.5 0.549h-0.78l-0.052-0.057 5.555-5.555h0.035l-0.017-0.014z"/>
</svg>
<span className="hidden sm:inline">Vim Mode</span>
<span className="hidden sm:inline">Vim Modus</span>
</button>
</div>
</div>
{/* Split Layout */}
<div className="flex flex-1 min-h-0" style={{ userSelect: dragRef.current ? "none" : undefined }}>
<div className="flex flex-1 min-h-0" style={{ userSelect: isDragging ? "none" : undefined }}>
{/* Left: File browser + Editor */}
<div className="flex flex-col" style={{ width: browserOpen ? `${split}%` : '48px', minWidth: browserOpen ? 240 : 48, maxWidth: 900, background: "#fff" }}>
<div className="flex flex-row h-full bg-white" style={{ width: leftPaneWidth, minWidth: 0, maxWidth: '100%' }}>
{/* File Browser Collapsible Toggle */}
<button
className="w-full flex items-center gap-2 px-2 py-1 bg-gray-100 border-b border-gray-200 text-gray-700 font-mono hover:bg-gray-200 focus:outline-none"
onClick={() => setBrowserOpen(o => !o)}
style={{ fontFamily: 'JetBrains Mono, monospace' }}
>
<span className="text-lg">{browserOpen ? '▼' : '▶'}</span>
<span className="font-bold text-sm">Files</span>
</button>
<div style={{ width: 32, minWidth: 32, maxWidth: 32, display: 'flex', flexDirection: 'column' }}>
<button
className={`w-full flex items-center justify-center px-2 py-1 font-mono hover:bg-gray-200 focus:outline-none ${
browserOpen
? 'bg-gray-100 border-b border-gray-200 text-gray-700'
: 'bg-gray-200 border-b border-gray-300 text-gray-600 hover:bg-gray-300'
}`}
onClick={() => setBrowserOpen(o => !o)}
style={{ fontFamily: 'JetBrains Mono, monospace', height: 40 }}
title={browserOpen ? "Datei-Explorer ausblenden" : "Datei-Explorer anzeigen"}
>
<span className="text-lg" style={{ transform: browserOpen ? 'rotate(0deg)' : 'rotate(90deg)' }}>
</span>
</button>
</div>
{/* File browser content */}
{browserOpen && (
<div className="h-48 border-b border-gray-200 p-2 overflow-auto bg-gray-50 text-gray-800 font-mono">
{tree.length === 0 ? (
<div className="text-xs text-gray-400">No files found.</div>
) : (
<FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} />
)}
<div className="border-r border-gray-200 bg-gray-50 text-gray-800 font-mono overflow-auto" style={{ width: fileBrowserWidth, minWidth: fileBrowserWidth, maxWidth: fileBrowserWidth }}>
<div className="h-64 p-2">
{tree.length === 0 ? (
<div className="text-xs text-gray-400">Keine Datein gefunden.</div>
) : (
<FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} />
)}
</div>
</div>
)}
{/* Monaco Editor */}
@@ -301,7 +496,7 @@ export default function EditorPage() {
fontFamily: 'JetBrains Mono',
fontWeight: 'bold',
fontSize: 15,
minimap: { enabled: false },
minimap: { enabled: true },
wordWrap: "on",
scrollBeyondLastLine: false,
smoothScrolling: true,
@@ -314,6 +509,16 @@ export default function EditorPage() {
cursorStyle: "line",
fixedOverflowWidgets: true,
readOnly: loading,
folding: true,
foldingStrategy: "indentation",
showFoldingControls: "always",
foldingHighlight: true,
foldingImportsByDefault: true,
unfoldOnClickAfterEndOfLine: false,
links: true,
colorDecorators: true,
formatOnPaste: true,
formatOnType: true,
}}
onChange={v => setFileContent(v ?? "")}
onMount={handleEditorDidMount}
@@ -324,11 +529,18 @@ export default function EditorPage() {
</div>
</div>
</div>
{/* Draggable Splitter */}
{/* Draggable Splitter - always show */}
<div
className="w-2 cursor-col-resize bg-gray-200 hover:bg-gray-300 transition-colors"
className={`w-1 cursor-col-resize transition-colors relative ${
isDragging ? 'bg-blue-500' : 'bg-gray-300 hover:bg-gray-400'
}`}
onMouseDown={onDragStart}
/>
>
{/* Drag handle indicator */}
<div className="absolute inset-y-0 left-1/2 transform -translate-x-1/2 w-4 flex items-center justify-center">
<div className="w-1 h-8 bg-gray-400 rounded-full opacity-50"></div>
</div>
</div>
{/* Right: Live Preview */}
<div className="flex-1 bg-gray-50 p-8 overflow-auto border-l border-gray-200">
<article className="bg-white rounded-lg shadow-sm border p-6 sm:p-8">
@@ -345,6 +557,45 @@ export default function EditorPage() {
</article>
</div>
</div>
{/* Unsaved Changes Dialog */}
{showUnsavedDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Ungespeicherte Änderungen</h3>
</div>
<p className="text-gray-600 mb-6">
Sie haben ungespeicherte Änderungen. Möchten Sie diese speichern, bevor Sie fortfahren?
</p>
<div className="flex gap-3 justify-end">
<button
onClick={() => handleUnsavedDialogAction('cancel')}
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
Abbrechen
</button>
<button
onClick={() => handleUnsavedDialogAction('discard')}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
>
Verwerfen
</button>
<button
onClick={() => handleUnsavedDialogAction('save')}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Speichern
</button>
</div>
</div>
</div>
)}
</div>
);
}
@@ -363,7 +614,7 @@ class MonacoErrorBoundary extends React.Component<{children: React.ReactNode}, {
}
render() {
if (this.state.error) {
return <div className="text-red-600 p-4">Editor error: {this.state.error.message}</div>;
return <div className="text-red-600 p-4">Fehler: {this.state.error.message}</div>;
}
return this.props.children;
}

View File

@@ -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() {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3">
<h2 className="text-base font-semibold">Parser Logs</h2>
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={reinterpretAllPosts}
className="px-2.5 py-1.5 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs transition-colors"
title="Force reinterpret all posts"
>
Reinterpret All
</button>
<button
onClick={clearLogs}
className="px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors"

View File

@@ -11,7 +11,7 @@ export const dynamic = "force-dynamic";
*
* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault
*
* Push later when on local Network. (//5jul25)
* Push later when on local Network. (//5jul25) ## Already done
**********************************************/
import { useState, useEffect, useCallback, useRef } from 'react';
@@ -904,7 +904,7 @@ export default function AdminPage() {
<a
href="/admin/editor"
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-gray-700 to-blue-700 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-gray-800 hover:to-blue-800 transition-all focus:outline-none focus:ring-2 focus:ring-blue-400"
title="Markdown Editor (VS Code Style)"
title="Markdown Bearbeiter"
style={{ minWidth: '160px' }}
>
{/* VS Code SVG Icon */}
@@ -950,8 +950,8 @@ export default function AdminPage() {
</defs>
</svg>
<span className="flex flex-col items-start">
<span>Editor</span>
<span className="text-xs font-normal text-blue-100">VS Code</span>
<span>Markdown Editor</span>
<span className="text-xs font-normal text-blue-100">Visual Studio Code</span>
</span>
</a>
{rememberExportChoice && lastExportChoice && (

View File

@@ -110,6 +110,46 @@ export async function GET(request: Request) {
});
}
}
const reinterpretAll = searchParams.get('reinterpretAll');
if (reinterpretAll === '1') {
// Call the Rust backend to force reinterpret all posts
const rustResult = spawnSync(
process.cwd() + '/markdown_backend/target/release/markdown_backend',
['reinterpret-all'],
{ 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' },
});
}
}
const reparsePost = searchParams.get('reparsePost');
if (reparsePost) {
// Call the Rust backend to reparse a specific post
const rustResult = spawnSync(
process.cwd() + '/markdown_backend/target/release/markdown_backend',
['reparse-post', reparsePost],
{ 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
try {
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');