Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ 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';
import { useAppStore } from './store/useAppStore';
import { useAutoUpdateCheck } from './components/UpdateChecker';
import { logger } from './services/logger';
import { UpdateNotificationBanner } from './components/UpdateNotificationBanner';
import { backend } from './services/backendAdapter';
import { syncFromBackend, startAutoSync, stopAutoSync } from './services/autoSync';
Expand Down Expand Up @@ -101,6 +103,14 @@ function App() {

useAutoUpdateCheck();

// Restore persisted frontend debug level at startup so capture is active
// app-wide, not only after DiagnosticLogsPanel mounts.
useEffect(() => {
if (sessionStorage.getItem('gsm:frontend-debug') === 'true') {
logger.setLevel('debug');
}
}, []);

useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
Expand Down Expand Up @@ -193,6 +203,7 @@ function App() {
{currentViewContent}
</main>
<BackToTop />
<DebugModeIndicator />
</div>
);
}
Expand Down
72 changes: 72 additions & 0 deletions src/components/DebugModeIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useState, useEffect, useCallback } from 'react';
import { logger } from '../services/logger';
import { backend } from '../services/backendAdapter';
import { useAppStore } from '../store/useAppStore';

/**
* Global debug mode indicator — fixed bottom-right corner.
* Reads debug state from sessionStorage; visible across all pages.
* Click to disable all debug modes and navigate to diagnostic logs.
*/
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.
const setCurrentView = useAppStore(s => s.setCurrentView);

// 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 handleClick = 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);

// Navigate to settings → logs tab
setCurrentView('settings');
// Notify SettingsPanel to switch to logs tab
window.dispatchEvent(new CustomEvent('gsm:navigate-to-settings-tab', { detail: { tab: 'logs' } }));
}, [setCurrentView]);

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

return (
<button
onClick={handleClick}
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 and open logs / 点击关闭调试并打开日志"
>
<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>
);
};
12 changes: 11 additions & 1 deletion src/components/ForkTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Package, Search, X, RefreshCw, ChevronLeft, ChevronRight, ChevronsLeft,
import { ForkRepo, WorkflowDefinition } from '../types';
import { useAppStore } from '../store/useAppStore';
import { GitHubApiService } from '../services/githubApi';
import { logger } from '../services/logger';
import { formatDistanceToNow } from 'date-fns';
import ForkCard from './ForkCard';
import { useDialog } from '../hooks/useDialog';
Expand Down Expand Up @@ -158,10 +159,12 @@ export const ForkTimeline: React.FC = () => {
return;
}

const startTime = Date.now();
setForkIsRefreshing(true);
try {
const githubApi = new GitHubApiService(githubToken);
const newForks = await githubApi.getUserForks();
logger.info('githubApi', 'Refresh forks completed', { forkCount: newForks.length, durationMs: Date.now() - startTime });

// Merge with existing forks, preserving read status
const existingForkMap = new Map(forks.map(f => [f.id, f]));
Expand Down Expand Up @@ -266,6 +269,7 @@ export const ForkTimeline: React.FC = () => {
}
} catch (error) {
console.error('Fork refresh failed:', error);
logger.error('githubApi', 'Refresh forks failed', { error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startTime });
toast(language === 'zh'
? 'Fork刷新失败,请检查网络连接。'
: 'Fork refresh failed. Please check your network connection.',
Expand Down Expand Up @@ -352,13 +356,15 @@ export const ForkTimeline: React.FC = () => {
if (!fork) return;

const { owner, repo, branch } = syncModal;
const syncStartTime = Date.now();

setSyncModal(prev => ({ ...prev, isOpen: false }));
setSyncingForks(prev => new Set(prev).add(fork.id));

try {
const githubApi = new GitHubApiService(githubToken);
const result = await githubApi.syncFork(owner, repo, branch);
logger.info('githubApi', 'Sync fork completed', { repo: fork.full_name, mergeType: result.mergeType, durationMs: Date.now() - syncStartTime });

// Mark fork as up-to-date in UI
setNeedsSyncMap(prev => ({ ...prev, [fork.id]: false }));
Expand All @@ -378,6 +384,7 @@ export const ForkTimeline: React.FC = () => {
}
} catch (error) {
console.error('Sync failed:', error);
logger.error('githubApi', 'Sync fork failed', { repo: fork.full_name, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - syncStartTime });
const errorMsg = error instanceof Error ? error.message : String(error);
if (errorMsg === 'NOT_A_FORK') {
toast(language === 'zh'
Expand Down Expand Up @@ -417,11 +424,13 @@ export const ForkTimeline: React.FC = () => {
if (!fork) return;

const branch = fork.default_branch || 'main';
const workflowStartTime = Date.now();
setRunningWorkflows(prev => new Set(prev).add(forkId));
try {
const [owner, repo] = fork.full_name.split('/');
const githubApi = new GitHubApiService(githubToken);
await githubApi.triggerWorkflowRun(owner, repo, workflowPath, branch);
logger.info('githubApi', 'Trigger workflow completed', { repo: fork.full_name, workflow: workflowName, branch, durationMs: Date.now() - workflowStartTime });

toast(language === 'zh'
? `已触发工作流 "${workflowName}" 在 ${branch} 分支。`
Expand All @@ -433,6 +442,7 @@ export const ForkTimeline: React.FC = () => {
await loadWorkflows(forkId);
} catch (error) {
console.error('Failed to run workflow:', error);
logger.error('githubApi', 'Trigger workflow failed', { repo: fork.full_name, workflow: workflowName, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - workflowStartTime });
toast(language === 'zh'
? `运行工作流失败。`
: `Failed to run workflow.`,
Expand Down
21 changes: 20 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 @@ -250,6 +252,16 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({
};
}, []);

// Listen for external tab navigation requests (e.g. from DebugModeIndicator)
useEffect(() => {
const onNavigate = (e: Event) => {
const tab = (e as CustomEvent<{ tab: SettingsTab }>).detail?.tab;
if (tab) handleTabChange(tab);
};
window.addEventListener('gsm:navigate-to-settings-tab', onNavigate);
return () => window.removeEventListener('gsm:navigate-to-settings-tab', onNavigate);
}, [handleTabChange]);

const tabs: SettingsTabItem[] = [
{
id: 'general',
Expand Down Expand Up @@ -286,6 +298,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 +327,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