Skip to main content
Glama
joelmnz

Article Manager MCP Server

by joelmnz
App.tsx13.2 kB
import React, { useState, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import { Login } from './components/Login'; import { Header } from './components/Header'; import { GlobalError } from './components/GlobalError'; import { Home } from './pages/Home'; import { ArticleView } from './pages/ArticleView'; import { ArticleEdit } from './pages/ArticleEdit'; import { RAGStatus } from './pages/RAGStatus'; import { ImportFiles } from './pages/ImportFiles'; import { PublicArticleView } from './components/PublicArticleView'; import { initializeRuntimeConfig, getRuntimeConfig, isRuntimeConfigAvailable, addConfigListener, type RuntimeConfig } from './utils/runtimeConfig'; import { parseRouteFromUrl, buildRouteUrl, buildAssetUrl, getBasePath, isRuntimeConfigAvailable as isUrlBuilderConfigAvailable } from './utils/urlBuilder'; import { configureApiClient, apiClient } from './utils/apiClient'; import './styles/main.css'; type Route = | { type: 'home' } | { type: 'article'; filename: string } | { type: 'edit'; filename: string } | { type: 'new' } | { type: 'rag-status' } | { type: 'import-files' } | { type: 'public-article'; slug: string }; function App() { const [token, setToken] = useState<string | null>(null); const [theme, setTheme] = useState<'light' | 'dark'>('dark'); const [route, setRoute] = useState<Route>({ type: 'home' }); const [intendedRoute, setIntendedRoute] = useState<Route | null>(null); const [runtimeConfig, setRuntimeConfig] = useState<RuntimeConfig | null>(null); const [configInitialized, setConfigInitialized] = useState(false); const [isHealthy, setIsHealthy] = useState<boolean | null>(null); const [healthError, setHealthError] = useState<any>(null); useEffect(() => { // Check backend health const checkHealth = async () => { try { const response = await apiClient.get('/health'); if (!response.ok) { const data = await response.json().catch(() => ({})); // If 503, it's a specific health failure (like DB down) // If 404 or other, it might be a routing issue, but still critical throw new Error(data.message || data.error || `Server health check failed with status ${response.status}`); } setIsHealthy(true); } catch (error) { console.error('System health check failed:', error); setIsHealthy(false); setHealthError(error instanceof Error ? error.message : 'Unknown error'); } }; // Initialize runtime configuration first with enhanced error handling const configResult = initializeRuntimeConfig(); if (!configResult.isValid) { console.warn('Runtime configuration issues:', configResult.errors); // Implement fallback behavior when runtime configuration is unavailable if (configResult.errors.includes('No runtime configuration injected by server')) { console.log('Falling back to root path behavior - application will work at root path only'); } } // Store the configuration for use throughout the component const config = getRuntimeConfig(); setRuntimeConfig(config); // Configure API client with runtime configuration configureApiClient(config); // Check backend health using the configured API client checkHealth(); setConfigInitialized(true); // Log configuration status for debugging console.log('Runtime configuration status:', { isAvailable: isRuntimeConfigAvailable(), config: config, urlBuilderConfigAvailable: isUrlBuilderConfigAvailable() }); // Listen for configuration changes (useful for dynamic updates) const unsubscribe = addConfigListener((newConfig) => { console.log('Runtime configuration updated:', newConfig); setRuntimeConfig(newConfig); // Reconfigure API client with new configuration configureApiClient(newConfig); // Re-parse current route with new configuration const currentPath = parseRouteFromUrl(window.location.href); const parsedRoute = parseRoute(currentPath); setRoute(parsedRoute); }); // Load saved token and theme const savedToken = localStorage.getItem('auth_token'); const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null; if (savedToken) { setToken(savedToken); } if (savedTheme) { setTheme(savedTheme); } // Parse initial route from URL using runtime configuration const path = parseRouteFromUrl(window.location.href); const parsedRoute = parseRoute(path); // Allow public article routes without authentication if (!savedToken && parsedRoute.type !== 'home' && parsedRoute.type !== 'public-article') { setIntendedRoute(parsedRoute); } else { setRoute(parsedRoute); } // Handle browser back/forward with runtime configuration awareness const handlePopState = () => { const currentPath = parseRouteFromUrl(window.location.href); const newRoute = parseRoute(currentPath); setRoute(newRoute); }; window.addEventListener('popstate', handlePopState); // Register service worker for PWA support with runtime base path if ('serviceWorker' in navigator) { try { const swUrl = buildAssetUrl('/sw.js'); const basePath = getBasePath(); // Set service worker scope to match the base path const registrationOptions: RegistrationOptions = {}; if (basePath) { // Scope must end with / for proper path matching registrationOptions.scope = basePath + '/'; } navigator.serviceWorker .register(swUrl, registrationOptions) .then((registration) => { console.log('Service Worker registered with runtime base path:', { url: swUrl, scope: registration.scope, basePath: basePath || '/' }); }) .catch((error) => { console.error('Service Worker registration failed:', error); // Continue operation even if service worker fails }); } catch (error) { console.error('Error building service worker URL:', error); // Fallback: try to register without base path navigator.serviceWorker .register('/sw.js') .catch((fallbackError) => { console.error('Fallback service worker registration also failed:', fallbackError); }); } } // Cleanup return () => { window.removeEventListener('popstate', handlePopState); unsubscribe(); }; }, []); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]); const parseRoute = (path: string): Route => { // Normalize path and handle empty/root cases // Strip query parameters for route matching const pathWithoutQuery = path.split('?')[0] || '/'; const normalizedPath = pathWithoutQuery || '/'; if (normalizedPath === '/' || normalizedPath === '') { return { type: 'home' }; } if (normalizedPath === '/rag-status') { return { type: 'rag-status' }; } if (normalizedPath === '/import-files') { return { type: 'import-files' }; } if (normalizedPath.startsWith('/public-article/')) { const slug = normalizedPath.replace('/public-article/', ''); if (slug) { return { type: 'public-article', slug }; } } if (normalizedPath.startsWith('/article/')) { const filename = normalizedPath.replace('/article/', ''); if (filename) { return { type: 'article', filename }; } } if (normalizedPath.startsWith('/edit/')) { const filename = normalizedPath.replace('/edit/', ''); if (filename) { return { type: 'edit', filename }; } } if (normalizedPath === '/new') { return { type: 'new' }; } // Default fallback to home for unrecognized routes console.warn(`Unrecognized route: ${normalizedPath}, falling back to home`); return { type: 'home' }; }; const navigate = (path: string) => { try { const newRoute = parseRoute(path); setRoute(newRoute); // Build full URL with base path for browser history using runtime configuration const fullUrl = buildRouteUrl(path); window.history.pushState({}, '', fullUrl); } catch (error) { console.error('Navigation error:', error); // Fallback behavior: try to navigate without base path try { const newRoute = parseRoute(path); setRoute(newRoute); window.history.pushState({}, '', path); } catch (fallbackError) { console.error('Fallback navigation also failed:', fallbackError); // Last resort: navigate to home setRoute({ type: 'home' }); window.history.pushState({}, '', '/'); } } }; const handleLogin = (newToken: string) => { setToken(newToken); localStorage.setItem('auth_token', newToken); // Navigate to intended route if exists if (intendedRoute) { setRoute(intendedRoute); setIntendedRoute(null); } }; const handleLogout = () => { setToken(null); localStorage.removeItem('auth_token'); setRoute({ type: 'home' }); navigate('/'); }; const handleThemeToggle = () => { const newTheme = theme === 'dark' ? 'light' : 'dark'; setTheme(newTheme); localStorage.setItem('theme', newTheme); }; // Show loading state while runtime configuration or health check is being initialized if (!configInitialized || isHealthy === null) { return ( <div className="app"> <main className="main" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', fontSize: '1.2rem' }}> Initializing application... </main> </div> ); } // Show global error if system is unhealthy if (isHealthy === false) { const isDbError = healthError?.includes('Database') || healthError?.includes('connect'); return ( <GlobalError title="System Initialization Failed" message={isDbError ? "The application cannot connect to the database. This usually means the database container is not running or the password configuration is incorrect." : "The application server found a critical issue during startup."} details={{ error: healthError }} onRetry={() => window.location.reload()} /> ); } // Show configuration error if runtime config is completely unavailable if (!runtimeConfig && !isRuntimeConfigAvailable()) { console.error('Critical: Runtime configuration is not available and fallback failed'); return ( <div className="app"> <main className="main" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '100vh', padding: '2rem', textAlign: 'center' }}> <h2>Configuration Error</h2> <p>The application could not initialize properly. Please check the server configuration.</p> <button onClick={() => window.location.reload()} style={{ marginTop: '1rem', padding: '0.5rem 1rem', fontSize: '1rem', cursor: 'pointer' }} > Retry </button> </main> </div> ); } if (!token) { // Allow access to public article view without authentication if (route.type === 'public-article') { return ( <div className="app"> <main className="main"> <PublicArticleView slug={route.slug} onNavigate={navigate} /> </main> </div> ); } return <Login onLogin={handleLogin} />; } return ( <div className="app"> <Header theme={theme} onThemeToggle={handleThemeToggle} onLogout={handleLogout} onNavigate={navigate} /> <main className="main"> {route.type === 'home' && ( <Home token={token} onNavigate={navigate} /> )} {route.type === 'rag-status' && ( <RAGStatus token={token} onNavigate={navigate} /> )} {route.type === 'import-files' && ( <ImportFiles token={token} onNavigate={navigate} /> )} {route.type === 'article' && ( <ArticleView filename={route.filename} token={token} onNavigate={navigate} /> )} {route.type === 'edit' && ( <ArticleEdit filename={route.filename} token={token} onNavigate={navigate} /> )} {route.type === 'new' && ( <ArticleEdit token={token} onNavigate={navigate} /> )} {route.type === 'public-article' && ( <PublicArticleView slug={route.slug} onNavigate={navigate} /> )} </main> </div> ); } const root = createRoot(document.getElementById('root')!); root.render(<App />);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joelmnz/mcp-markdown-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server