diff --git a/backend/internal/api/handlers/logs_handler.go b/backend/internal/api/handlers/logs_handler.go index d23dcc5a..d9ca204c 100644 --- a/backend/internal/api/handlers/logs_handler.go +++ b/backend/internal/api/handlers/logs_handler.go @@ -39,6 +39,7 @@ func (h *LogsHandler) Read(c *gin.Context) { Search: c.Query("search"), Host: c.Query("host"), Status: c.Query("status"), + Level: c.Query("level"), Limit: limit, Offset: offset, } diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go index 6c9e2d0f..28c7d12c 100644 --- a/backend/internal/caddy/config.go +++ b/backend/internal/caddy/config.go @@ -23,7 +23,7 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin Logging: &LoggingConfig{ Logs: map[string]*LogConfig{ "access": { - Level: "INFO", + Level: "DEBUG", Writer: &WriterConfig{ Output: "file", Filename: logFile, diff --git a/backend/internal/caddy/config_test.go b/backend/internal/caddy/config_test.go index 5600db94..97e19a47 100644 --- a/backend/internal/caddy/config_test.go +++ b/backend/internal/caddy/config_test.go @@ -119,18 +119,15 @@ func TestGenerateConfig_Logging(t *testing.T) { config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") require.NoError(t, err) - // Verify logging config + // Verify logging configuration require.NotNil(t, config.Logging) require.NotNil(t, config.Logging.Logs) - require.Contains(t, config.Logging.Logs, "access") - - logConfig := config.Logging.Logs["access"] - require.Equal(t, "INFO", logConfig.Level) - require.NotNil(t, logConfig.Writer) - require.Equal(t, "file", logConfig.Writer.Output) - require.Contains(t, logConfig.Writer.Filename, "access.log") - require.NotNil(t, logConfig.Writer.RollSize) - require.NotNil(t, logConfig.Writer.RollKeep) + require.NotNil(t, config.Logging.Logs["access"]) + require.Equal(t, "DEBUG", config.Logging.Logs["access"].Level) + require.Contains(t, config.Logging.Logs["access"].Writer.Filename, "access.log") + require.Equal(t, 10, config.Logging.Logs["access"].Writer.RollSize) + require.Equal(t, 5, config.Logging.Logs["access"].Writer.RollKeep) + require.Equal(t, 7, config.Logging.Logs["access"].Writer.RollKeepDays) } func TestGenerateConfig_Advanced(t *testing.T) { diff --git a/backend/internal/models/log_entry.go b/backend/internal/models/log_entry.go index 52e95592..465b776d 100644 --- a/backend/internal/models/log_entry.go +++ b/backend/internal/models/log_entry.go @@ -36,6 +36,7 @@ type LogFilter struct { Search string `form:"search"` Host string `form:"host"` Status string `form:"status"` // e.g., "200", "4xx", "5xx" + Level string `form:"level"` Limit int `form:"limit"` Offset int `form:"offset"` } diff --git a/backend/internal/services/log_service.go b/backend/internal/services/log_service.go index 6c9563c6..cdb155ea 100644 --- a/backend/internal/services/log_service.go +++ b/backend/internal/services/log_service.go @@ -175,6 +175,13 @@ func (s *LogService) matchesFilter(entry models.CaddyAccessLog, filter models.Lo } } + // Level Filter + if filter.Level != "" { + if !strings.EqualFold(entry.Level, filter.Level) { + return false + } + } + // Host Filter if filter.Host != "" { if !strings.Contains(strings.ToLower(entry.Request.Host), strings.ToLower(filter.Host)) { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index abb72698..7e7ff45f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,8 +9,6 @@ import ProxyHosts from './pages/ProxyHosts' import RemoteServers from './pages/RemoteServers' import ImportCaddy from './pages/ImportCaddy' import Certificates from './pages/Certificates' -import SettingsLayout from './pages/SettingsLayout' -import TasksLayout from './pages/TasksLayout' import SystemSettings from './pages/SystemSettings' import Account from './pages/Account' import Backups from './pages/Backups' @@ -43,14 +41,14 @@ export default function App() { } /> {/* Settings Routes */} - }> + } /> } /> } /> {/* Tasks Routes */} - }> + } /> } /> } /> diff --git a/frontend/src/api/logs.ts b/frontend/src/api/logs.ts index 1fb9bda0..8e42af9f 100644 --- a/frontend/src/api/logs.ts +++ b/frontend/src/api/logs.ts @@ -35,6 +35,7 @@ export interface LogFilter { search?: string; host?: string; status?: string; + level?: string; limit?: number; offset?: number; } @@ -49,6 +50,7 @@ export const getLogContent = async (filename: string, filter: LogFilter = {}): P if (filter.search) params.append('search', filter.search); if (filter.host) params.append('host', filter.host); if (filter.status) params.append('status', filter.status); + if (filter.level) params.append('level', filter.level); if (filter.limit) params.append('limit', filter.limit.toString()); if (filter.offset) params.append('offset', filter.offset.toString()); diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 1c97c9d7..4ecaa19c 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -39,12 +39,26 @@ export default function Layout({ children }: LayoutProps) { { name: 'Domains', path: '/domains', icon: '🌍' }, { name: 'Certificates', path: '/certificates', icon: '🔒' }, { name: 'Import Caddyfile', path: '/import', icon: '📥' }, - { name: 'Settings', path: '/settings/system', icon: '⚙️' }, - { name: 'Tasks', path: '/tasks/backups', icon: '📋' }, + { + name: 'Settings', + icon: '⚙️', + children: [ + { name: 'System', path: '/settings/system', icon: '⚙️' }, + { name: 'Account', path: '/settings/account', icon: '🛡️' }, + ] + }, + { + name: 'Tasks', + icon: '📋', + children: [ + { name: 'Backups', path: '/tasks/backups', icon: '💾' }, + { name: 'Logs', path: '/tasks/logs', icon: '📝' }, + ] + }, ] return ( - + {/* Mobile Header */} CPM+ @@ -71,14 +85,49 @@ export default function Layout({ children }: LayoutProps) { {navigation.map((item) => { - const isActive = location.pathname === item.path || - (item.path.startsWith('/settings') && location.pathname.startsWith('/settings') && item.path === '/settings/system') || - (item.path.startsWith('/tasks') && location.pathname.startsWith('/tasks')) + if (item.children) { + // Group Header + return ( + + {!isCollapsed && ( + + {item.name} + + )} + {isCollapsed && ( + + )} + + {item.children.map((child) => { + const isActive = location.pathname === child.path + return ( + setMobileSidebarOpen(false)} + className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${ + isActive + ? 'bg-blue-100 text-blue-700 dark:bg-blue-active dark:text-white' + : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white' + } ${isCollapsed ? 'justify-center' : ''}`} + title={isCollapsed ? child.name : ''} + > + {child.icon} + {!isCollapsed && child.name} + + ) + })} + + + ) + } + + const isActive = location.pathname === item.path return ( setMobileSidebarOpen(false)} className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${ isActive diff --git a/frontend/src/components/LogFilters.tsx b/frontend/src/components/LogFilters.tsx index 613876f9..9ea617ba 100644 --- a/frontend/src/components/LogFilters.tsx +++ b/frontend/src/components/LogFilters.tsx @@ -7,6 +7,8 @@ interface LogFiltersProps { onSearchChange: (value: string) => void; status: string; onStatusChange: (value: string) => void; + level: string; + onLevelChange: (value: string) => void; host: string; onHostChange: (value: string) => void; onRefresh: () => void; @@ -19,6 +21,8 @@ export const LogFilters: React.FC = ({ onSearchChange, status, onStatusChange, + level, + onLevelChange, host, onHostChange, onRefresh, @@ -50,6 +54,20 @@ export const LogFilters: React.FC = ({ /> + + onLevelChange(e.target.value)} + className="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white" + > + All Levels + Debug + Info + Warn + Error + + + { const [search, setSearch] = useState(''); const [host, setHost] = useState(''); const [status, setStatus] = useState(''); + const [level, setLevel] = useState(''); const [page, setPage] = useState(0); const limit = 50; @@ -33,12 +34,13 @@ const Logs: React.FC = () => { search, host, status, + level, limit, offset: page * limit }; const { data: logData, isLoading: isLoadingContent, refetch: refetchContent } = useQuery({ - queryKey: ['logContent', selectedLog, search, host, status, page], + queryKey: ['logContent', selectedLog, search, host, status, level, page], queryFn: () => selectedLog ? getLogContent(selectedLog, filter) : Promise.resolve(null), enabled: !!selectedLog, }); @@ -104,6 +106,8 @@ const Logs: React.FC = () => { onHostChange={(v) => { setHost(v); setPage(0); }} status={status} onStatusChange={(v) => { setStatus(v); setPage(0); }} + level={level} + onLevelChange={(v) => { setLevel(v); setPage(0); }} onRefresh={refetchContent} onDownload={handleDownload} isLoading={isLoadingContent} diff --git a/frontend/src/pages/SettingsLayout.tsx b/frontend/src/pages/SettingsLayout.tsx deleted file mode 100644 index 43c3e7ab..00000000 --- a/frontend/src/pages/SettingsLayout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Outlet, Link, useLocation } from 'react-router-dom' -import { Shield, Server } from 'lucide-react' - -export default function SettingsLayout() { - const location = useLocation() - - const isActive = (path: string) => location.pathname === path - - return ( - - {/* Settings Sidebar */} - - - - Settings - - - - - System - - - - Account - - - - - - {/* Content Area */} - - - - - ) -} diff --git a/frontend/src/pages/TasksLayout.tsx b/frontend/src/pages/TasksLayout.tsx deleted file mode 100644 index 50fbf84c..00000000 --- a/frontend/src/pages/TasksLayout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Outlet, Link, useLocation } from 'react-router-dom' -import { Archive, FileText } from 'lucide-react' - -export default function TasksLayout() { - const location = useLocation() - - const isActive = (path: string) => location.pathname === path - - return ( - - {/* Tasks Sidebar */} - - - - Tasks - - - - - Backups - - - - Logs - - - - - - {/* Content Area */} - - - - - ) -} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index d40ee0be..06c7952b 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -8,6 +8,7 @@ export default { theme: { extend: { colors: { + 'light-bg': '#f0f4f8', // Light greyish blue 'dark-bg': '#0f172a', 'dark-sidebar': '#020617', 'dark-card': '#1e293b',