/** * T109, T120: Settings page with user profile, API token, and index health */ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowLeft, Copy, RefreshCw, Check } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; import { SettingsSectionSkeleton } from '@/components/SettingsSectionSkeleton'; import { getCurrentUser, getToken, logout, getStoredToken, isDemoSession, AUTH_TOKEN_CHANGED_EVENT } from '@/services/auth'; import { getIndexHealth, rebuildIndex, type RebuildResponse } from '@/services/api'; import type { User } from '@/types/user'; import type { IndexHealth } from '@/types/search'; import { SystemLogs } from '@/components/SystemLogs'; export function Settings() { const navigate = useNavigate(); const [user, setUser] = useState(null); const [apiToken, setApiToken] = useState(''); const [indexHealth, setIndexHealth] = useState(null); const [copied, setCopied] = useState(false); const [isRebuilding, setIsRebuilding] = useState(false); const [rebuildResult, setRebuildResult] = useState(null); const [error, setError] = useState(null); const [isDemoMode, setIsDemoMode] = useState(isDemoSession()); useEffect(() => { loadData(); }, []); useEffect(() => { const handler = () => setIsDemoMode(isDemoSession()); window.addEventListener(AUTH_TOKEN_CHANGED_EVENT, handler); return () => window.removeEventListener(AUTH_TOKEN_CHANGED_EVENT, handler); }, []); const loadData = async () => { try { const token = getStoredToken(); // Handle local-dev-token as a special case if (token === 'local-dev-token') { setUser({ user_id: 'demo-user', vault_path: '/data/vaults/demo-user', created: new Date().toISOString(), }); setApiToken(token); } else { // Real OAuth user const userData = await getCurrentUser().catch(() => null); setUser(userData); if (token) { setApiToken(token); } } // Always try to load index health const health = await getIndexHealth().catch(() => null); setIndexHealth(health); } catch (err) { console.error('Error loading settings:', err); } }; const handleGenerateToken = async () => { if (isDemoMode) { setError('Demo mode is read-only. Sign in to generate new tokens.'); return; } try { setError(null); const tokenResponse = await getToken(); setApiToken(tokenResponse.token); } catch (err) { setError('Failed to generate token'); console.error('Error generating token:', err); } }; const handleCopyToken = async () => { try { await navigator.clipboard.writeText(apiToken); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy token:', err); } }; const handleRebuildIndex = async () => { if (isDemoMode) { setError('Demo mode is read-only. Sign in to rebuild the index.'); return; } setIsRebuilding(true); setError(null); setRebuildResult(null); try { const result = await rebuildIndex(); setRebuildResult(result); // Reload health data const health = await getIndexHealth(); setIndexHealth(health); } catch (err) { setError('Failed to rebuild index'); console.error('Error rebuilding index:', err); } finally { setIsRebuilding(false); } }; const formatDate = (dateString: string | null) => { if (!dateString) return 'Never'; return new Date(dateString).toLocaleString(); }; const getUserInitials = (userId: string) => { return userId.slice(0, 2).toUpperCase(); }; return (
{/* Header */}

Settings

{/* Content */}
{isDemoMode && ( You are viewing the shared demo vault. Sign in with Hugging Face from the main app to enable token generation and index management. )} {error && ( {error} )} {/* Profile */} {user ? ( Profile Your account information
{getUserInitials(user.user_id)}
{user.hf_profile?.name || user.hf_profile?.username || user.user_id}
User ID: {user.user_id}
Vault: {user.vault_path}
) : ( )} {/* API Token */} API Token for MCP Use this token to configure MCP clients (Claude Desktop, etc.)

MCP Configuration (Hosted HTTP):

{`{
  "mcpServers": {
    "obsidian-docs": {
      "transport": "http",
      "url": "${window.location.origin}/mcp",
      "headers": {
        "Authorization": "Bearer ${apiToken || 'YOUR_TOKEN_HERE'}"
      }
    }
  }
}`}
              

Local Development (STDIO):

{`{
  "mcpServers": {
    "obsidian-docs": {
      "command": "python",
      "args": ["-m", "backend.src.mcp.server"],
      "cwd": "/absolute/path/to/Document-MCP",
      "env": {
        "LOCAL_USER_ID": "local-dev",
        "PYTHONPATH": "/absolute/path/to/Document-MCP",
        "FASTMCP_SHOW_CLI_BANNER": "false"
      }
    }
  }
}`}
              

Replace /absolute/path/to/Document-MCP with your local checkout path

{/* Index Health */} {indexHealth ? ( Index Health Full-text search index status and maintenance
Notes Indexed
{indexHealth.note_count}
Last Updated
{formatDate(indexHealth.last_incremental_update)}
Last Full Rebuild
{formatDate(indexHealth.last_full_rebuild)}
{rebuildResult && ( Index rebuilt successfully! Indexed {rebuildResult.notes_indexed} notes in {rebuildResult.duration_ms}ms )}
Rebuilding the index will re-scan all notes and update the full-text search database. This may take a few seconds for large vaults.
) : ( )} {/* System Logs */}
); }