Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions server/src/routes/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const router = Router();

const ALLOWED_LEVELS: readonly LogLevel[] = ['debug', 'info', 'warn', 'error'];

// Note: Authentication is handled by authMiddleware applied to all /api/* routes in index.ts

// GET /api/logs — returns recent backend log entries
router.get('/api/logs', (req, res) => {
try {
Expand Down Expand Up @@ -40,4 +42,33 @@ router.get('/api/logs', (req, res) => {
}
});

// DELETE /api/logs — clear all backend log entries
router.delete('/api/logs', (_req, res) => {
try {
logger.clear();
res.json({ success: true });
} catch (err) {
logger.errorFromError('logs.clearLogs', 'Failed to clear logs', err);
res.status(500).json({ error: 'Failed to clear logs', code: 'CLEAR_LOGS_FAILED' });
}
});

// GET /api/logs/debug — return current debug mode status
router.get('/api/logs/debug', (_req, res) => {
res.json({ debugMode: logger.isDebugMode() });
});

// POST /api/logs/debug — toggle backend debug mode
router.post('/api/logs/debug', (req, res) => {
try {
const enabled = req.body.enabled === true;
logger.setLevel(enabled ? 'debug' : 'info');
logger.info('logs.debug', enabled ? 'Backend debug mode enabled' : 'Backend debug mode disabled');
res.json({ success: true, debugMode: logger.isDebugMode() });
} catch (err) {
logger.errorFromError('logs.debugToggle', 'Failed to toggle debug mode', err);
res.status(500).json({ error: 'Failed to toggle debug mode', code: 'DEBUG_TOGGLE_FAILED' });
}
});

export default router;
16 changes: 16 additions & 0 deletions server/src/services/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ class Logger {
setLevel(level: LogLevel): void {
this.minLevel = level;
}

getLevel(): LogLevel {
return this.minLevel;
}

isDebugMode(): boolean {
return this.minLevel === 'debug';
}

getModules(): string[] {
const modules = new Set<string>();
for (const entry of this.buffer) {
modules.add(entry.module);
}
return Array.from(modules).sort();
}
}

export const logger = new Logger();
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CategorySidebar } from './components/CategorySidebar';
import { ReleaseTimeline } from './components/ReleaseTimeline';
import { ForkTimeline } from './components/ForkTimeline';
import { SettingsPanel } from './components/SettingsPanel';
import { DebugModeIndicator } from './components/DebugModeIndicator';
import { DiscoveryView } from './components/DiscoveryView';
import { BackToTop } from './components/BackToTop';
import { ErrorBoundary } from './components/ErrorBoundary';
Expand Down Expand Up @@ -193,6 +194,7 @@ function App() {
{currentViewContent}
</main>
<BackToTop />
<DebugModeIndicator />
</div>
);
}
Expand Down
65 changes: 65 additions & 0 deletions src/components/DebugModeIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useState, useEffect, useCallback } from 'react';
import { logger } from '../services/logger';
import { backend } from '../services/backendAdapter';

/**
* Global debug mode indicator — fixed bottom-right corner.
* Reads debug state from sessionStorage; visible across all pages.
* Click to quickly disable all debug modes.
*/
export const DebugModeIndicator: React.FC = () => {
const [frontendDebug, setFrontendDebug] = useState(() => sessionStorage.getItem('gsm:frontend-debug') === 'true');
const [backendDebug, setBackendDebug] = useState(() => sessionStorage.getItem('gsm:backend-debug') === 'true');
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Sync with sessionStorage changes (e.g. from DiagnosticLogsPanel)
useEffect(() => {
const check = () => {
setFrontendDebug(sessionStorage.getItem('gsm:frontend-debug') === 'true');
setBackendDebug(sessionStorage.getItem('gsm:backend-debug') === 'true');
};
// Also listen for storage events from other tabs
window.addEventListener('storage', check);
// Poll for changes (sessionStorage doesn't fire storage events in same tab)
const interval = setInterval(check, 2000);
return () => {
window.removeEventListener('storage', check);
clearInterval(interval);
};
}, []);

const disableAll = useCallback(async () => {
// Disable frontend debug
logger.setLevel('info');
sessionStorage.setItem('gsm:frontend-debug', 'false');
setFrontendDebug(false);

// Disable backend debug
if (backend.isAvailable) {
try {
const secret = sessionStorage.getItem('github-stars-manager-backend-secret');
await fetch('/api/logs/debug', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${secret}` },
body: JSON.stringify({ enabled: false }),
});
} catch { /* Backend unreachable */ }
}
sessionStorage.setItem('gsm:backend-debug', 'false');
setBackendDebug(false);
}, []);

if (!frontendDebug && !backendDebug) return null;

return (
<button
onClick={disableAll}
className="fixed bottom-6 right-6 z-50 flex items-center space-x-2 px-3 py-2 bg-green-500 text-white rounded-full shadow-lg hover:bg-green-600 transition-colors text-sm font-medium cursor-pointer"
title="Click to disable debug mode / 点击关闭调试模式"
>
<span className="w-2 h-2 rounded-full bg-white animate-pulse" />
<span>DEBUG</span>
{frontendDebug && <span className="text-xs opacity-80">FE</span>}
{backendDebug && <span className="text-xs opacity-80">BE</span>}
</button>
);
};
11 changes: 10 additions & 1 deletion src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
X,
Trash2,
Wifi,
ScrollText,
} from 'lucide-react';
import { useAppStore } from '../store/useAppStore';
import { isElectron } from '../services/electronProxy';
Expand All @@ -23,9 +24,10 @@ import {
CategoryPanel,
DataManagementPanel,
NetworkPanel,
DiagnosticLogsPanel,
} from './settings';

type SettingsTab = 'general' | 'ai' | 'webdav' | 'backup' | 'backend' | 'category' | 'data' | 'network';
type SettingsTab = 'general' | 'ai' | 'webdav' | 'backup' | 'backend' | 'category' | 'data' | 'logs' | 'network';

interface SettingsTabItem {
id: SettingsTab;
Expand Down Expand Up @@ -286,6 +288,11 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({
label: t('数据管理', 'Data Management'),
icon: <Trash2 className="w-5 h-5" />,
},
{
id: 'logs',
label: t('诊断日志', 'Diagnostic Logs'),
icon: <ScrollText className="w-5 h-5" />,
},
...((isElectron() || backend.isAvailable) ? [{
id: 'network' as SettingsTab,
label: t('网络设置', 'Network'),
Expand All @@ -310,6 +317,8 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({
return <CategoryPanel t={t} />;
case 'data':
return <DataManagementPanel t={t} />;
case 'logs':
return <DiagnosticLogsPanel t={t} />;
case 'network':
return <NetworkPanel t={t} />;
default:
Expand Down
Loading
Loading