@payez/next-mvp 4.1.9 → 4.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +792 -792
- package/dist/api-handlers/auth/refresh.js +38 -15
- package/dist/auth/better-auth.js +7 -18
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/lib/ensure-fresh-access-token.js +30 -7
- package/dist/lib/idp-fetch.js +25 -9
- package/dist/lib/session-store.d.ts +1 -0
- package/dist/lib/session-store.js +12 -8
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +893 -893
- package/scripts/dev-broker.ps1 +35 -35
- package/scripts/dev-local.ps1 +45 -45
- package/src/api/auth-handler.ts +548 -548
- package/src/api-handlers/account/change-password.ts +142 -142
- package/src/api-handlers/account/update-phone.ts +79 -79
- package/src/api-handlers/account/verify-email.ts +125 -125
- package/src/api-handlers/account/verify-sms.ts +125 -125
- package/src/api-handlers/admin/analytics.ts +443 -443
- package/src/api-handlers/admin/audit.ts +223 -223
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +251 -251
- package/src/api-handlers/admin/sessions.ts +318 -318
- package/src/api-handlers/admin/site-logs.ts +365 -365
- package/src/api-handlers/admin/users.ts +242 -242
- package/src/api-handlers/admin/vibe-data.ts +324 -324
- package/src/api-handlers/auth/refresh.ts +705 -682
- package/src/api-handlers/auth/signout.ts +199 -199
- package/src/api-handlers/auth/status.ts +20 -20
- package/src/api-handlers/auth/update-session.ts +110 -110
- package/src/api-handlers/auth/verify-code.ts +112 -112
- package/src/api-handlers/session/refresh-viability.ts +36 -36
- package/src/api-handlers/test/force-expire.ts +60 -60
- package/src/auth/auth-decision.ts +229 -229
- package/src/auth/better-auth.ts +7 -18
- package/src/auth/route-config.ts +219 -219
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/utils/idp-client.ts +445 -445
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +143 -143
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +266 -266
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +505 -505
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +414 -414
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/FederatedAuthSection.tsx +95 -95
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
- package/src/components/auth/TraditionalAuthSection.tsx +99 -99
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +97 -97
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +57 -57
- package/src/config/logger.ts +62 -62
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +97 -97
- package/src/hooks/useAvailableProviders.ts +120 -120
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/usePublicAuthSettings.ts +149 -149
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +336 -336
- package/src/index.ts +60 -60
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +178 -178
- package/src/lib/ensure-fresh-access-token.ts +358 -335
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +26 -10
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- package/src/lib/session-store.ts +4 -1
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +663 -663
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/pages/admin-login/page.tsx +221 -221
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +456 -456
- package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +234 -234
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +565 -565
- package/src/pages/security/index.ts +9 -9
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
- package/src/pages/test-env/JwtInspectPage.tsx +456 -456
- package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
- package/src/pages/test-env/TestEnvPage.tsx +118 -118
- package/src/pages/test-env/index.ts +25 -25
- package/src/pages/verify-code/page.tsx +652 -652
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/auth/logout.ts +108 -108
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/settings.ts +75 -75
- package/src/routes/auth/viability.ts +224 -224
- package/src/server/auth-guard.ts +170 -170
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/with-auth.ts +93 -93
- package/src/stores/authStore.ts +1899 -1899
- package/src/templates/instrumentation.ts +41 -41
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +590 -590
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +185 -185
- package/src/vibe/generic.ts +431 -431
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +132 -132
- package/src/vibe/sessions.ts +551 -551
- package/src/vibe/types.ts +577 -577
|
@@ -1,665 +1,665 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* =============================================================================
|
|
3
|
-
* VIBE ADMIN LOGGING SETTINGS TAB
|
|
4
|
-
* =============================================================================
|
|
5
|
-
*
|
|
6
|
-
* Admin UI for managing log levels, retention, and storage limits.
|
|
7
|
-
* Provides environment presets, per-category levels, and manual pruning.
|
|
8
|
-
*
|
|
9
|
-
* =============================================================================
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
'use client';
|
|
13
|
-
|
|
14
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
15
|
-
import {
|
|
16
|
-
Settings,
|
|
17
|
-
Zap,
|
|
18
|
-
Server,
|
|
19
|
-
Bug,
|
|
20
|
-
Trash2,
|
|
21
|
-
HardDrive,
|
|
22
|
-
Clock,
|
|
23
|
-
RefreshCw,
|
|
24
|
-
ChevronDown,
|
|
25
|
-
AlertCircle,
|
|
26
|
-
Check,
|
|
27
|
-
} from 'lucide-react';
|
|
28
|
-
|
|
29
|
-
// -----------------------------------------------------------------------------
|
|
30
|
-
// TYPES
|
|
31
|
-
// -----------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'critical';
|
|
34
|
-
|
|
35
|
-
export interface LogLevels {
|
|
36
|
-
api: LogLevel;
|
|
37
|
-
auth: LogLevel;
|
|
38
|
-
database: LogLevel;
|
|
39
|
-
agent: LogLevel;
|
|
40
|
-
system: LogLevel;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface RetentionSettings {
|
|
44
|
-
debug_days: number;
|
|
45
|
-
info_days: number;
|
|
46
|
-
warn_days: number;
|
|
47
|
-
error_days: number;
|
|
48
|
-
critical_days: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface StorageLimits {
|
|
52
|
-
max_size_mb: number;
|
|
53
|
-
max_rows: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface UsageStats {
|
|
57
|
-
total_rows: number;
|
|
58
|
-
total_size_mb: number;
|
|
59
|
-
by_level?: {
|
|
60
|
-
debug?: { rows: number; size_mb: number };
|
|
61
|
-
info?: { rows: number; size_mb: number };
|
|
62
|
-
warn?: { rows: number; size_mb: number };
|
|
63
|
-
error?: { rows: number; size_mb: number };
|
|
64
|
-
critical?: { rows: number; size_mb: number };
|
|
65
|
-
};
|
|
66
|
-
limits: StorageLimits;
|
|
67
|
-
percent_used: number;
|
|
68
|
-
oldest_entry?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface LoggingSettings {
|
|
72
|
-
levels: LogLevels;
|
|
73
|
-
retention: RetentionSettings;
|
|
74
|
-
limits: StorageLimits;
|
|
75
|
-
current_usage?: UsageStats;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface LoggingSettingsTabProps {
|
|
79
|
-
isDark?: boolean;
|
|
80
|
-
/** API base path (default: /api/admin/logging) */
|
|
81
|
-
apiBasePath?: string;
|
|
82
|
-
/** Callback when settings are saved */
|
|
83
|
-
onSave?: (settings: Partial<LoggingSettings>) => void;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// -----------------------------------------------------------------------------
|
|
87
|
-
// CONSTANTS
|
|
88
|
-
// -----------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
const LOG_LEVELS: { value: LogLevel; label: string; color: string }[] = [
|
|
91
|
-
{ value: 'debug', label: 'Debug', color: 'text-gray-400' },
|
|
92
|
-
{ value: 'info', label: 'Info', color: 'text-blue-400' },
|
|
93
|
-
{ value: 'warn', label: 'Warning', color: 'text-amber-400' },
|
|
94
|
-
{ value: 'error', label: 'Error', color: 'text-red-400' },
|
|
95
|
-
{ value: 'critical', label: 'Critical', color: 'text-red-500' },
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
const PRESETS: {
|
|
99
|
-
id: string;
|
|
100
|
-
label: string;
|
|
101
|
-
description: string;
|
|
102
|
-
levels: LogLevels;
|
|
103
|
-
}[] = [
|
|
104
|
-
{
|
|
105
|
-
id: 'development',
|
|
106
|
-
label: 'Development',
|
|
107
|
-
description: 'Verbose logging for debugging',
|
|
108
|
-
levels: { api: 'debug', auth: 'debug', database: 'info', agent: 'debug', system: 'info' },
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: 'production',
|
|
112
|
-
label: 'Production',
|
|
113
|
-
description: 'Minimal logging for performance',
|
|
114
|
-
levels: { api: 'warn', auth: 'info', database: 'error', agent: 'info', system: 'warn' },
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
id: 'troubleshooting',
|
|
118
|
-
label: 'Troubleshooting',
|
|
119
|
-
description: 'Maximum verbosity for issue diagnosis',
|
|
120
|
-
levels: { api: 'debug', auth: 'debug', database: 'debug', agent: 'debug', system: 'debug' },
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
const CATEGORIES: { id: keyof LogLevels; label: string; description: string }[] = [
|
|
125
|
-
{ id: 'api', label: 'API', description: 'Request/response logging' },
|
|
126
|
-
{ id: 'auth', label: 'Authentication', description: 'Login, logout, token events' },
|
|
127
|
-
{ id: 'database', label: 'Database', description: 'Query and connection logging' },
|
|
128
|
-
{ id: 'agent', label: 'Agent', description: 'AI agent execution logs' },
|
|
129
|
-
{ id: 'system', label: 'System', description: 'Infrastructure and health events' },
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
const RETENTION_DEFAULTS: RetentionSettings = {
|
|
133
|
-
debug_days: 7,
|
|
134
|
-
info_days: 30,
|
|
135
|
-
warn_days: 60,
|
|
136
|
-
error_days: 90,
|
|
137
|
-
critical_days: 180,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const PRUNE_OPTIONS = [
|
|
141
|
-
{ value: 'debug_all', label: 'Clear all Debug logs' },
|
|
142
|
-
{ value: 'older_7', label: 'Clear logs older than 7 days' },
|
|
143
|
-
{ value: 'older_30', label: 'Clear logs older than 30 days' },
|
|
144
|
-
{ value: 'all_keep_100', label: 'Clear all (keep last 100)' },
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
// -----------------------------------------------------------------------------
|
|
148
|
-
// COMPONENT
|
|
149
|
-
// -----------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
export function LoggingSettingsTab({
|
|
152
|
-
isDark = true,
|
|
153
|
-
apiBasePath = '/api/admin/logging',
|
|
154
|
-
onSave,
|
|
155
|
-
}: LoggingSettingsTabProps) {
|
|
156
|
-
const [settings, setSettings] = useState<LoggingSettings | null>(null);
|
|
157
|
-
const [usage, setUsage] = useState<UsageStats | null>(null);
|
|
158
|
-
const [loading, setLoading] = useState(true);
|
|
159
|
-
const [saving, setSaving] = useState(false);
|
|
160
|
-
const [pruning, setPruning] = useState(false);
|
|
161
|
-
const [error, setError] = useState<string | null>(null);
|
|
162
|
-
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
163
|
-
const [hasChanges, setHasChanges] = useState(false);
|
|
164
|
-
|
|
165
|
-
// Edited state (local changes before save)
|
|
166
|
-
const [editedLevels, setEditedLevels] = useState<LogLevels | null>(null);
|
|
167
|
-
const [editedRetention, setEditedRetention] = useState<RetentionSettings | null>(null);
|
|
168
|
-
const [editedLimits, setEditedLimits] = useState<StorageLimits | null>(null);
|
|
169
|
-
|
|
170
|
-
const themeClasses = {
|
|
171
|
-
bg: isDark ? 'bg-slate-950' : 'bg-gray-50',
|
|
172
|
-
cardBg: isDark ? 'bg-slate-800 border-slate-700' : 'bg-white border-gray-200 shadow-sm',
|
|
173
|
-
textPrimary: isDark ? 'text-white' : 'text-gray-900',
|
|
174
|
-
textSecondary: isDark ? 'text-gray-400' : 'text-gray-600',
|
|
175
|
-
textMuted: isDark ? 'text-gray-500' : 'text-gray-500',
|
|
176
|
-
inputBg: isDark ? 'bg-slate-900 border-slate-600' : 'bg-white border-gray-300',
|
|
177
|
-
hoverBg: isDark ? 'hover:bg-slate-700' : 'hover:bg-gray-100',
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// Fetch settings
|
|
181
|
-
const fetchSettings = useCallback(async () => {
|
|
182
|
-
setLoading(true);
|
|
183
|
-
setError(null);
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const res = await fetch(`${apiBasePath}/settings`);
|
|
187
|
-
if (res.ok) {
|
|
188
|
-
const data = await res.json();
|
|
189
|
-
setSettings(data);
|
|
190
|
-
setEditedLevels(data.levels);
|
|
191
|
-
setEditedRetention(data.retention);
|
|
192
|
-
setEditedLimits(data.limits);
|
|
193
|
-
if (data.current_usage) {
|
|
194
|
-
setUsage(data.current_usage);
|
|
195
|
-
}
|
|
196
|
-
} else if (res.status === 404) {
|
|
197
|
-
// Endpoint not ready - use defaults
|
|
198
|
-
const defaults: LoggingSettings = {
|
|
199
|
-
levels: PRESETS[1].levels, // Production defaults
|
|
200
|
-
retention: RETENTION_DEFAULTS,
|
|
201
|
-
limits: { max_size_mb: 10, max_rows: 20000 },
|
|
202
|
-
};
|
|
203
|
-
setSettings(defaults);
|
|
204
|
-
setEditedLevels(defaults.levels);
|
|
205
|
-
setEditedRetention(defaults.retention);
|
|
206
|
-
setEditedLimits(defaults.limits);
|
|
207
|
-
} else {
|
|
208
|
-
throw new Error('Failed to fetch settings');
|
|
209
|
-
}
|
|
210
|
-
} catch (err: any) {
|
|
211
|
-
setError(err.message);
|
|
212
|
-
} finally {
|
|
213
|
-
setLoading(false);
|
|
214
|
-
}
|
|
215
|
-
}, [apiBasePath]);
|
|
216
|
-
|
|
217
|
-
// Fetch usage separately (can be more frequent)
|
|
218
|
-
const fetchUsage = useCallback(async () => {
|
|
219
|
-
try {
|
|
220
|
-
const res = await fetch(`${apiBasePath}/usage`);
|
|
221
|
-
if (res.ok) {
|
|
222
|
-
const data = await res.json();
|
|
223
|
-
setUsage(data);
|
|
224
|
-
}
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error('Failed to fetch usage:', err);
|
|
227
|
-
}
|
|
228
|
-
}, [apiBasePath]);
|
|
229
|
-
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
fetchSettings();
|
|
232
|
-
}, [fetchSettings]);
|
|
233
|
-
|
|
234
|
-
// Check for changes
|
|
235
|
-
useEffect(() => {
|
|
236
|
-
if (!settings || !editedLevels || !editedRetention || !editedLimits) {
|
|
237
|
-
setHasChanges(false);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const levelsChanged = JSON.stringify(settings.levels) !== JSON.stringify(editedLevels);
|
|
242
|
-
const retentionChanged = JSON.stringify(settings.retention) !== JSON.stringify(editedRetention);
|
|
243
|
-
const limitsChanged = JSON.stringify(settings.limits) !== JSON.stringify(editedLimits);
|
|
244
|
-
|
|
245
|
-
setHasChanges(levelsChanged || retentionChanged || limitsChanged);
|
|
246
|
-
}, [settings, editedLevels, editedRetention, editedLimits]);
|
|
247
|
-
|
|
248
|
-
// Apply preset
|
|
249
|
-
const applyPreset = async (presetId: string) => {
|
|
250
|
-
const preset = PRESETS.find(p => p.id === presetId);
|
|
251
|
-
if (!preset) return;
|
|
252
|
-
|
|
253
|
-
setEditedLevels(preset.levels);
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const res = await fetch(`${apiBasePath}/preset`, {
|
|
257
|
-
method: 'POST',
|
|
258
|
-
headers: { 'Content-Type': 'application/json' },
|
|
259
|
-
body: JSON.stringify({ preset: presetId }),
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
if (res.ok) {
|
|
263
|
-
setSuccessMessage(`Applied ${preset.label} preset`);
|
|
264
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
265
|
-
fetchSettings();
|
|
266
|
-
}
|
|
267
|
-
} catch (err) {
|
|
268
|
-
// Preset applied locally even if API fails
|
|
269
|
-
console.error('Failed to apply preset:', err);
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
// Save settings
|
|
274
|
-
const saveSettings = async () => {
|
|
275
|
-
if (!hasChanges || !editedLevels || !editedRetention || !editedLimits) return;
|
|
276
|
-
|
|
277
|
-
setSaving(true);
|
|
278
|
-
setError(null);
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
const payload: Partial<LoggingSettings> = {
|
|
282
|
-
levels: editedLevels,
|
|
283
|
-
retention: editedRetention,
|
|
284
|
-
limits: editedLimits,
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
const res = await fetch(`${apiBasePath}/settings`, {
|
|
288
|
-
method: 'PUT',
|
|
289
|
-
headers: { 'Content-Type': 'application/json' },
|
|
290
|
-
body: JSON.stringify(payload),
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
if (res.ok) {
|
|
294
|
-
setSuccessMessage('Settings saved successfully');
|
|
295
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
296
|
-
onSave?.(payload);
|
|
297
|
-
fetchSettings();
|
|
298
|
-
} else if (res.status === 404) {
|
|
299
|
-
// Endpoint not ready - simulate success
|
|
300
|
-
setSettings({
|
|
301
|
-
levels: editedLevels,
|
|
302
|
-
retention: editedRetention,
|
|
303
|
-
limits: editedLimits,
|
|
304
|
-
});
|
|
305
|
-
setSuccessMessage('Settings saved (demo mode)');
|
|
306
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
307
|
-
} else {
|
|
308
|
-
throw new Error('Failed to save settings');
|
|
309
|
-
}
|
|
310
|
-
} catch (err: any) {
|
|
311
|
-
setError(err.message);
|
|
312
|
-
} finally {
|
|
313
|
-
setSaving(false);
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
// Manual prune
|
|
318
|
-
const handlePrune = async (option: string) => {
|
|
319
|
-
setPruning(true);
|
|
320
|
-
setError(null);
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
let body: { older_than_days?: number; level?: string } = {};
|
|
324
|
-
|
|
325
|
-
switch (option) {
|
|
326
|
-
case 'debug_all':
|
|
327
|
-
body = { level: 'debug' };
|
|
328
|
-
break;
|
|
329
|
-
case 'older_7':
|
|
330
|
-
body = { older_than_days: 7 };
|
|
331
|
-
break;
|
|
332
|
-
case 'older_30':
|
|
333
|
-
body = { older_than_days: 30 };
|
|
334
|
-
break;
|
|
335
|
-
case 'all_keep_100':
|
|
336
|
-
body = { older_than_days: 0 }; // Server interprets as "all"
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const res = await fetch(`${apiBasePath}/prune`, {
|
|
341
|
-
method: 'POST',
|
|
342
|
-
headers: { 'Content-Type': 'application/json' },
|
|
343
|
-
body: JSON.stringify(body),
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
if (res.ok) {
|
|
347
|
-
const data = await res.json();
|
|
348
|
-
setSuccessMessage(`Pruned ${data.deleted_rows?.toLocaleString() || 'some'} rows`);
|
|
349
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
350
|
-
fetchUsage();
|
|
351
|
-
} else if (res.status === 404) {
|
|
352
|
-
setSuccessMessage('Prune completed (demo mode)');
|
|
353
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
354
|
-
} else {
|
|
355
|
-
throw new Error('Failed to prune logs');
|
|
356
|
-
}
|
|
357
|
-
} catch (err: any) {
|
|
358
|
-
setError(err.message);
|
|
359
|
-
} finally {
|
|
360
|
-
setPruning(false);
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
// Get active preset
|
|
365
|
-
const getActivePreset = () => {
|
|
366
|
-
if (!editedLevels) return null;
|
|
367
|
-
return PRESETS.find(p => JSON.stringify(p.levels) === JSON.stringify(editedLevels))?.id || null;
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
if (loading) {
|
|
371
|
-
return (
|
|
372
|
-
<div className="flex items-center justify-center py-12">
|
|
373
|
-
<RefreshCw className="w-8 h-8 animate-spin text-indigo-500" />
|
|
374
|
-
</div>
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return (
|
|
379
|
-
<div className="space-y-6">
|
|
380
|
-
{/* Error/Success Messages */}
|
|
381
|
-
{error && (
|
|
382
|
-
<div className={`flex items-center gap-3 p-4 rounded-lg ${isDark ? 'bg-red-900/30 border-red-700' : 'bg-red-50 border-red-300'} border`}>
|
|
383
|
-
<AlertCircle className={isDark ? 'text-red-400' : 'text-red-500'} size={20} />
|
|
384
|
-
<span className={isDark ? 'text-red-300' : 'text-red-700'}>{error}</span>
|
|
385
|
-
</div>
|
|
386
|
-
)}
|
|
387
|
-
|
|
388
|
-
{successMessage && (
|
|
389
|
-
<div className={`flex items-center gap-3 p-4 rounded-lg ${isDark ? 'bg-green-900/30 border-green-700' : 'bg-green-50 border-green-300'} border`}>
|
|
390
|
-
<Check className={isDark ? 'text-green-400' : 'text-green-500'} size={20} />
|
|
391
|
-
<span className={isDark ? 'text-green-300' : 'text-green-700'}>{successMessage}</span>
|
|
392
|
-
</div>
|
|
393
|
-
)}
|
|
394
|
-
|
|
395
|
-
{/* Section 1: Environment Presets */}
|
|
396
|
-
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
397
|
-
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
398
|
-
<div className="flex items-center gap-2">
|
|
399
|
-
<Zap className={isDark ? 'text-amber-400' : 'text-amber-500'} size={20} />
|
|
400
|
-
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Quick Setup</h3>
|
|
401
|
-
</div>
|
|
402
|
-
<p className={`text-sm ${themeClasses.textMuted} mt-1`}>
|
|
403
|
-
Choose a preset to quickly configure log levels
|
|
404
|
-
</p>
|
|
405
|
-
</div>
|
|
406
|
-
<div className="p-5">
|
|
407
|
-
<div className="flex flex-wrap gap-3">
|
|
408
|
-
{PRESETS.map((preset) => {
|
|
409
|
-
const isActive = getActivePreset() === preset.id;
|
|
410
|
-
return (
|
|
411
|
-
<button
|
|
412
|
-
key={preset.id}
|
|
413
|
-
onClick={() => applyPreset(preset.id)}
|
|
414
|
-
className={`
|
|
415
|
-
px-4 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
416
|
-
${isActive
|
|
417
|
-
? 'bg-indigo-600 text-white ring-2 ring-indigo-500 ring-offset-2 ' + (isDark ? 'ring-offset-slate-800' : 'ring-offset-white')
|
|
418
|
-
: (isDark ? 'bg-slate-700 text-white hover:bg-slate-600' : 'bg-gray-100 text-gray-700 hover:bg-gray-200')
|
|
419
|
-
}
|
|
420
|
-
`}
|
|
421
|
-
>
|
|
422
|
-
{preset.label}
|
|
423
|
-
</button>
|
|
424
|
-
);
|
|
425
|
-
})}
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
|
|
430
|
-
{/* Section 2: Category Log Levels */}
|
|
431
|
-
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
432
|
-
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
433
|
-
<div className="flex items-center gap-2">
|
|
434
|
-
<Bug className={isDark ? 'text-violet-400' : 'text-violet-500'} size={20} />
|
|
435
|
-
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Log Levels</h3>
|
|
436
|
-
</div>
|
|
437
|
-
<p className={`text-sm ${themeClasses.textMuted} mt-1`}>
|
|
438
|
-
Set verbosity per category
|
|
439
|
-
</p>
|
|
440
|
-
</div>
|
|
441
|
-
<div className="p-5">
|
|
442
|
-
<div className="space-y-4">
|
|
443
|
-
{CATEGORIES.map((category) => (
|
|
444
|
-
<div key={category.id} className="flex items-center justify-between">
|
|
445
|
-
<div>
|
|
446
|
-
<p className={`font-medium ${themeClasses.textPrimary}`}>{category.label}</p>
|
|
447
|
-
<p className={`text-xs ${themeClasses.textMuted}`}>{category.description}</p>
|
|
448
|
-
</div>
|
|
449
|
-
<select
|
|
450
|
-
value={editedLevels?.[category.id] || 'info'}
|
|
451
|
-
onChange={(e) => {
|
|
452
|
-
setEditedLevels(prev => prev ? { ...prev, [category.id]: e.target.value as LogLevel } : null);
|
|
453
|
-
}}
|
|
454
|
-
className={`
|
|
455
|
-
px-3 py-2 rounded-lg text-sm font-medium cursor-pointer
|
|
456
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
457
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
458
|
-
`}
|
|
459
|
-
>
|
|
460
|
-
{LOG_LEVELS.map((level) => (
|
|
461
|
-
<option key={level.value} value={level.value}>
|
|
462
|
-
{level.label}
|
|
463
|
-
</option>
|
|
464
|
-
))}
|
|
465
|
-
</select>
|
|
466
|
-
</div>
|
|
467
|
-
))}
|
|
468
|
-
</div>
|
|
469
|
-
</div>
|
|
470
|
-
</div>
|
|
471
|
-
|
|
472
|
-
{/* Section 3: Retention Settings */}
|
|
473
|
-
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
474
|
-
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
475
|
-
<div className="flex items-center gap-2">
|
|
476
|
-
<Clock className={isDark ? 'text-cyan-400' : 'text-cyan-500'} size={20} />
|
|
477
|
-
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Retention Settings</h3>
|
|
478
|
-
</div>
|
|
479
|
-
<p className={`text-sm ${themeClasses.textMuted} mt-1`}>
|
|
480
|
-
How long to keep logs by severity level
|
|
481
|
-
</p>
|
|
482
|
-
</div>
|
|
483
|
-
<div className="p-5">
|
|
484
|
-
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
|
485
|
-
{[
|
|
486
|
-
{ key: 'debug_days', label: 'Debug' },
|
|
487
|
-
{ key: 'info_days', label: 'Info' },
|
|
488
|
-
{ key: 'warn_days', label: 'Warnings' },
|
|
489
|
-
{ key: 'error_days', label: 'Errors' },
|
|
490
|
-
{ key: 'critical_days', label: 'Critical' },
|
|
491
|
-
].map((item) => (
|
|
492
|
-
<div key={item.key}>
|
|
493
|
-
<label className={`block text-sm ${themeClasses.textSecondary} mb-1`}>
|
|
494
|
-
{item.label}
|
|
495
|
-
</label>
|
|
496
|
-
<div className="flex items-center gap-2">
|
|
497
|
-
<input
|
|
498
|
-
type="number"
|
|
499
|
-
min="1"
|
|
500
|
-
max="365"
|
|
501
|
-
value={editedRetention?.[item.key as keyof RetentionSettings] || 7}
|
|
502
|
-
onChange={(e) => {
|
|
503
|
-
setEditedRetention(prev => prev ? { ...prev, [item.key]: parseInt(e.target.value) || 7 } : null);
|
|
504
|
-
}}
|
|
505
|
-
className={`
|
|
506
|
-
w-20 px-3 py-2 rounded-lg text-sm
|
|
507
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
508
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
509
|
-
`}
|
|
510
|
-
/>
|
|
511
|
-
<span className={`text-sm ${themeClasses.textMuted}`}>days</span>
|
|
512
|
-
</div>
|
|
513
|
-
</div>
|
|
514
|
-
))}
|
|
515
|
-
</div>
|
|
516
|
-
</div>
|
|
517
|
-
</div>
|
|
518
|
-
|
|
519
|
-
{/* Section 4: Storage & Pruning */}
|
|
520
|
-
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
521
|
-
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
522
|
-
<div className="flex items-center gap-2">
|
|
523
|
-
<HardDrive className={isDark ? 'text-emerald-400' : 'text-emerald-500'} size={20} />
|
|
524
|
-
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Storage Limits</h3>
|
|
525
|
-
</div>
|
|
526
|
-
</div>
|
|
527
|
-
<div className="p-5 space-y-6">
|
|
528
|
-
{/* Limit Inputs */}
|
|
529
|
-
<div className="flex flex-wrap items-center gap-6">
|
|
530
|
-
<div className="flex items-center gap-2">
|
|
531
|
-
<label className={`text-sm ${themeClasses.textSecondary}`}>Max size:</label>
|
|
532
|
-
<input
|
|
533
|
-
type="number"
|
|
534
|
-
min="1"
|
|
535
|
-
max="1000"
|
|
536
|
-
value={editedLimits?.max_size_mb || 10}
|
|
537
|
-
onChange={(e) => {
|
|
538
|
-
setEditedLimits(prev => prev ? { ...prev, max_size_mb: parseInt(e.target.value) || 10 } : null);
|
|
539
|
-
}}
|
|
540
|
-
className={`
|
|
541
|
-
w-20 px-3 py-2 rounded-lg text-sm
|
|
542
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
543
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
544
|
-
`}
|
|
545
|
-
/>
|
|
546
|
-
<span className={`text-sm ${themeClasses.textMuted}`}>MB</span>
|
|
547
|
-
</div>
|
|
548
|
-
<span className={themeClasses.textMuted}>or</span>
|
|
549
|
-
<div className="flex items-center gap-2">
|
|
550
|
-
<input
|
|
551
|
-
type="number"
|
|
552
|
-
min="100"
|
|
553
|
-
max="1000000"
|
|
554
|
-
step="1000"
|
|
555
|
-
value={editedLimits?.max_rows || 20000}
|
|
556
|
-
onChange={(e) => {
|
|
557
|
-
setEditedLimits(prev => prev ? { ...prev, max_rows: parseInt(e.target.value) || 20000 } : null);
|
|
558
|
-
}}
|
|
559
|
-
className={`
|
|
560
|
-
w-28 px-3 py-2 rounded-lg text-sm
|
|
561
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
562
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
563
|
-
`}
|
|
564
|
-
/>
|
|
565
|
-
<span className={`text-sm ${themeClasses.textMuted}`}>rows</span>
|
|
566
|
-
</div>
|
|
567
|
-
</div>
|
|
568
|
-
|
|
569
|
-
{/* Usage Bar */}
|
|
570
|
-
{usage && (
|
|
571
|
-
<div>
|
|
572
|
-
<div className="flex items-center justify-between mb-2">
|
|
573
|
-
<span className={`text-sm ${themeClasses.textSecondary}`}>Current usage</span>
|
|
574
|
-
<span className={`text-sm font-medium ${themeClasses.textPrimary}`}>
|
|
575
|
-
{usage.total_size_mb?.toFixed(1) || 0} MB of {usage.limits.max_size_mb} MB ({usage.percent_used || 0}%)
|
|
576
|
-
</span>
|
|
577
|
-
</div>
|
|
578
|
-
<div className={`h-3 rounded-full overflow-hidden ${isDark ? 'bg-slate-700' : 'bg-gray-200'}`}>
|
|
579
|
-
<div
|
|
580
|
-
className={`h-full rounded-full transition-all ${
|
|
581
|
-
(usage.percent_used || 0) > 90 ? 'bg-red-500' :
|
|
582
|
-
(usage.percent_used || 0) > 70 ? 'bg-amber-500' :
|
|
583
|
-
'bg-emerald-500'
|
|
584
|
-
}`}
|
|
585
|
-
style={{ width: `${Math.min(usage.percent_used || 0, 100)}%` }}
|
|
586
|
-
/>
|
|
587
|
-
</div>
|
|
588
|
-
<div className={`flex items-center justify-between mt-2 text-xs ${themeClasses.textMuted}`}>
|
|
589
|
-
<span>{(usage.total_rows || 0).toLocaleString()} rows</span>
|
|
590
|
-
{usage.oldest_entry && (
|
|
591
|
-
<span>Oldest: {new Date(usage.oldest_entry).toLocaleDateString()}</span>
|
|
592
|
-
)}
|
|
593
|
-
</div>
|
|
594
|
-
</div>
|
|
595
|
-
)}
|
|
596
|
-
|
|
597
|
-
{/* Prune Button */}
|
|
598
|
-
<div className="flex items-center gap-4">
|
|
599
|
-
<div className="relative">
|
|
600
|
-
<select
|
|
601
|
-
disabled={pruning}
|
|
602
|
-
onChange={(e) => {
|
|
603
|
-
if (e.target.value) {
|
|
604
|
-
handlePrune(e.target.value);
|
|
605
|
-
e.target.value = '';
|
|
606
|
-
}
|
|
607
|
-
}}
|
|
608
|
-
className={`
|
|
609
|
-
px-4 py-2.5 pr-10 rounded-lg text-sm font-medium cursor-pointer appearance-none
|
|
610
|
-
${isDark ? 'bg-red-900/30 text-red-300 border-red-700 hover:bg-red-900/50' : 'bg-red-50 text-red-700 border-red-300 hover:bg-red-100'}
|
|
611
|
-
border focus:outline-none focus:ring-2 focus:ring-red-500
|
|
612
|
-
disabled:opacity-50 disabled:cursor-not-allowed
|
|
613
|
-
`}
|
|
614
|
-
>
|
|
615
|
-
<option value="">Prune Now...</option>
|
|
616
|
-
{PRUNE_OPTIONS.map((opt) => (
|
|
617
|
-
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
618
|
-
))}
|
|
619
|
-
</select>
|
|
620
|
-
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${isDark ? 'text-red-400' : 'text-red-500'}`} />
|
|
621
|
-
</div>
|
|
622
|
-
{pruning && (
|
|
623
|
-
<span className={`flex items-center gap-2 text-sm ${themeClasses.textMuted}`}>
|
|
624
|
-
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
625
|
-
Pruning...
|
|
626
|
-
</span>
|
|
627
|
-
)}
|
|
628
|
-
</div>
|
|
629
|
-
</div>
|
|
630
|
-
</div>
|
|
631
|
-
|
|
632
|
-
{/* Save Button */}
|
|
633
|
-
<div className="flex items-center justify-end gap-4">
|
|
634
|
-
{hasChanges && (
|
|
635
|
-
<span className={`text-sm ${themeClasses.textMuted}`}>
|
|
636
|
-
You have unsaved changes
|
|
637
|
-
</span>
|
|
638
|
-
)}
|
|
639
|
-
<button
|
|
640
|
-
onClick={saveSettings}
|
|
641
|
-
disabled={!hasChanges || saving}
|
|
642
|
-
className={`
|
|
643
|
-
px-6 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
644
|
-
${hasChanges
|
|
645
|
-
? 'bg-indigo-600 text-white hover:bg-indigo-700'
|
|
646
|
-
: (isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-400')
|
|
647
|
-
}
|
|
648
|
-
disabled:cursor-not-allowed
|
|
649
|
-
`}
|
|
650
|
-
>
|
|
651
|
-
{saving ? (
|
|
652
|
-
<span className="flex items-center gap-2">
|
|
653
|
-
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
654
|
-
Saving...
|
|
655
|
-
</span>
|
|
656
|
-
) : (
|
|
657
|
-
'Save Settings'
|
|
658
|
-
)}
|
|
659
|
-
</button>
|
|
660
|
-
</div>
|
|
661
|
-
</div>
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
export default LoggingSettingsTab;
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* VIBE ADMIN LOGGING SETTINGS TAB
|
|
4
|
+
* =============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Admin UI for managing log levels, retention, and storage limits.
|
|
7
|
+
* Provides environment presets, per-category levels, and manual pruning.
|
|
8
|
+
*
|
|
9
|
+
* =============================================================================
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use client';
|
|
13
|
+
|
|
14
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
Settings,
|
|
17
|
+
Zap,
|
|
18
|
+
Server,
|
|
19
|
+
Bug,
|
|
20
|
+
Trash2,
|
|
21
|
+
HardDrive,
|
|
22
|
+
Clock,
|
|
23
|
+
RefreshCw,
|
|
24
|
+
ChevronDown,
|
|
25
|
+
AlertCircle,
|
|
26
|
+
Check,
|
|
27
|
+
} from 'lucide-react';
|
|
28
|
+
|
|
29
|
+
// -----------------------------------------------------------------------------
|
|
30
|
+
// TYPES
|
|
31
|
+
// -----------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'critical';
|
|
34
|
+
|
|
35
|
+
export interface LogLevels {
|
|
36
|
+
api: LogLevel;
|
|
37
|
+
auth: LogLevel;
|
|
38
|
+
database: LogLevel;
|
|
39
|
+
agent: LogLevel;
|
|
40
|
+
system: LogLevel;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RetentionSettings {
|
|
44
|
+
debug_days: number;
|
|
45
|
+
info_days: number;
|
|
46
|
+
warn_days: number;
|
|
47
|
+
error_days: number;
|
|
48
|
+
critical_days: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface StorageLimits {
|
|
52
|
+
max_size_mb: number;
|
|
53
|
+
max_rows: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UsageStats {
|
|
57
|
+
total_rows: number;
|
|
58
|
+
total_size_mb: number;
|
|
59
|
+
by_level?: {
|
|
60
|
+
debug?: { rows: number; size_mb: number };
|
|
61
|
+
info?: { rows: number; size_mb: number };
|
|
62
|
+
warn?: { rows: number; size_mb: number };
|
|
63
|
+
error?: { rows: number; size_mb: number };
|
|
64
|
+
critical?: { rows: number; size_mb: number };
|
|
65
|
+
};
|
|
66
|
+
limits: StorageLimits;
|
|
67
|
+
percent_used: number;
|
|
68
|
+
oldest_entry?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface LoggingSettings {
|
|
72
|
+
levels: LogLevels;
|
|
73
|
+
retention: RetentionSettings;
|
|
74
|
+
limits: StorageLimits;
|
|
75
|
+
current_usage?: UsageStats;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface LoggingSettingsTabProps {
|
|
79
|
+
isDark?: boolean;
|
|
80
|
+
/** API base path (default: /api/admin/logging) */
|
|
81
|
+
apiBasePath?: string;
|
|
82
|
+
/** Callback when settings are saved */
|
|
83
|
+
onSave?: (settings: Partial<LoggingSettings>) => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// -----------------------------------------------------------------------------
|
|
87
|
+
// CONSTANTS
|
|
88
|
+
// -----------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
const LOG_LEVELS: { value: LogLevel; label: string; color: string }[] = [
|
|
91
|
+
{ value: 'debug', label: 'Debug', color: 'text-gray-400' },
|
|
92
|
+
{ value: 'info', label: 'Info', color: 'text-blue-400' },
|
|
93
|
+
{ value: 'warn', label: 'Warning', color: 'text-amber-400' },
|
|
94
|
+
{ value: 'error', label: 'Error', color: 'text-red-400' },
|
|
95
|
+
{ value: 'critical', label: 'Critical', color: 'text-red-500' },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const PRESETS: {
|
|
99
|
+
id: string;
|
|
100
|
+
label: string;
|
|
101
|
+
description: string;
|
|
102
|
+
levels: LogLevels;
|
|
103
|
+
}[] = [
|
|
104
|
+
{
|
|
105
|
+
id: 'development',
|
|
106
|
+
label: 'Development',
|
|
107
|
+
description: 'Verbose logging for debugging',
|
|
108
|
+
levels: { api: 'debug', auth: 'debug', database: 'info', agent: 'debug', system: 'info' },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'production',
|
|
112
|
+
label: 'Production',
|
|
113
|
+
description: 'Minimal logging for performance',
|
|
114
|
+
levels: { api: 'warn', auth: 'info', database: 'error', agent: 'info', system: 'warn' },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'troubleshooting',
|
|
118
|
+
label: 'Troubleshooting',
|
|
119
|
+
description: 'Maximum verbosity for issue diagnosis',
|
|
120
|
+
levels: { api: 'debug', auth: 'debug', database: 'debug', agent: 'debug', system: 'debug' },
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const CATEGORIES: { id: keyof LogLevels; label: string; description: string }[] = [
|
|
125
|
+
{ id: 'api', label: 'API', description: 'Request/response logging' },
|
|
126
|
+
{ id: 'auth', label: 'Authentication', description: 'Login, logout, token events' },
|
|
127
|
+
{ id: 'database', label: 'Database', description: 'Query and connection logging' },
|
|
128
|
+
{ id: 'agent', label: 'Agent', description: 'AI agent execution logs' },
|
|
129
|
+
{ id: 'system', label: 'System', description: 'Infrastructure and health events' },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const RETENTION_DEFAULTS: RetentionSettings = {
|
|
133
|
+
debug_days: 7,
|
|
134
|
+
info_days: 30,
|
|
135
|
+
warn_days: 60,
|
|
136
|
+
error_days: 90,
|
|
137
|
+
critical_days: 180,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const PRUNE_OPTIONS = [
|
|
141
|
+
{ value: 'debug_all', label: 'Clear all Debug logs' },
|
|
142
|
+
{ value: 'older_7', label: 'Clear logs older than 7 days' },
|
|
143
|
+
{ value: 'older_30', label: 'Clear logs older than 30 days' },
|
|
144
|
+
{ value: 'all_keep_100', label: 'Clear all (keep last 100)' },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// -----------------------------------------------------------------------------
|
|
148
|
+
// COMPONENT
|
|
149
|
+
// -----------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function LoggingSettingsTab({
|
|
152
|
+
isDark = true,
|
|
153
|
+
apiBasePath = '/api/admin/logging',
|
|
154
|
+
onSave,
|
|
155
|
+
}: LoggingSettingsTabProps) {
|
|
156
|
+
const [settings, setSettings] = useState<LoggingSettings | null>(null);
|
|
157
|
+
const [usage, setUsage] = useState<UsageStats | null>(null);
|
|
158
|
+
const [loading, setLoading] = useState(true);
|
|
159
|
+
const [saving, setSaving] = useState(false);
|
|
160
|
+
const [pruning, setPruning] = useState(false);
|
|
161
|
+
const [error, setError] = useState<string | null>(null);
|
|
162
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
163
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
164
|
+
|
|
165
|
+
// Edited state (local changes before save)
|
|
166
|
+
const [editedLevels, setEditedLevels] = useState<LogLevels | null>(null);
|
|
167
|
+
const [editedRetention, setEditedRetention] = useState<RetentionSettings | null>(null);
|
|
168
|
+
const [editedLimits, setEditedLimits] = useState<StorageLimits | null>(null);
|
|
169
|
+
|
|
170
|
+
const themeClasses = {
|
|
171
|
+
bg: isDark ? 'bg-slate-950' : 'bg-gray-50',
|
|
172
|
+
cardBg: isDark ? 'bg-slate-800 border-slate-700' : 'bg-white border-gray-200 shadow-sm',
|
|
173
|
+
textPrimary: isDark ? 'text-white' : 'text-gray-900',
|
|
174
|
+
textSecondary: isDark ? 'text-gray-400' : 'text-gray-600',
|
|
175
|
+
textMuted: isDark ? 'text-gray-500' : 'text-gray-500',
|
|
176
|
+
inputBg: isDark ? 'bg-slate-900 border-slate-600' : 'bg-white border-gray-300',
|
|
177
|
+
hoverBg: isDark ? 'hover:bg-slate-700' : 'hover:bg-gray-100',
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Fetch settings
|
|
181
|
+
const fetchSettings = useCallback(async () => {
|
|
182
|
+
setLoading(true);
|
|
183
|
+
setError(null);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(`${apiBasePath}/settings`);
|
|
187
|
+
if (res.ok) {
|
|
188
|
+
const data = await res.json();
|
|
189
|
+
setSettings(data);
|
|
190
|
+
setEditedLevels(data.levels);
|
|
191
|
+
setEditedRetention(data.retention);
|
|
192
|
+
setEditedLimits(data.limits);
|
|
193
|
+
if (data.current_usage) {
|
|
194
|
+
setUsage(data.current_usage);
|
|
195
|
+
}
|
|
196
|
+
} else if (res.status === 404) {
|
|
197
|
+
// Endpoint not ready - use defaults
|
|
198
|
+
const defaults: LoggingSettings = {
|
|
199
|
+
levels: PRESETS[1].levels, // Production defaults
|
|
200
|
+
retention: RETENTION_DEFAULTS,
|
|
201
|
+
limits: { max_size_mb: 10, max_rows: 20000 },
|
|
202
|
+
};
|
|
203
|
+
setSettings(defaults);
|
|
204
|
+
setEditedLevels(defaults.levels);
|
|
205
|
+
setEditedRetention(defaults.retention);
|
|
206
|
+
setEditedLimits(defaults.limits);
|
|
207
|
+
} else {
|
|
208
|
+
throw new Error('Failed to fetch settings');
|
|
209
|
+
}
|
|
210
|
+
} catch (err: any) {
|
|
211
|
+
setError(err.message);
|
|
212
|
+
} finally {
|
|
213
|
+
setLoading(false);
|
|
214
|
+
}
|
|
215
|
+
}, [apiBasePath]);
|
|
216
|
+
|
|
217
|
+
// Fetch usage separately (can be more frequent)
|
|
218
|
+
const fetchUsage = useCallback(async () => {
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(`${apiBasePath}/usage`);
|
|
221
|
+
if (res.ok) {
|
|
222
|
+
const data = await res.json();
|
|
223
|
+
setUsage(data);
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error('Failed to fetch usage:', err);
|
|
227
|
+
}
|
|
228
|
+
}, [apiBasePath]);
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
fetchSettings();
|
|
232
|
+
}, [fetchSettings]);
|
|
233
|
+
|
|
234
|
+
// Check for changes
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!settings || !editedLevels || !editedRetention || !editedLimits) {
|
|
237
|
+
setHasChanges(false);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const levelsChanged = JSON.stringify(settings.levels) !== JSON.stringify(editedLevels);
|
|
242
|
+
const retentionChanged = JSON.stringify(settings.retention) !== JSON.stringify(editedRetention);
|
|
243
|
+
const limitsChanged = JSON.stringify(settings.limits) !== JSON.stringify(editedLimits);
|
|
244
|
+
|
|
245
|
+
setHasChanges(levelsChanged || retentionChanged || limitsChanged);
|
|
246
|
+
}, [settings, editedLevels, editedRetention, editedLimits]);
|
|
247
|
+
|
|
248
|
+
// Apply preset
|
|
249
|
+
const applyPreset = async (presetId: string) => {
|
|
250
|
+
const preset = PRESETS.find(p => p.id === presetId);
|
|
251
|
+
if (!preset) return;
|
|
252
|
+
|
|
253
|
+
setEditedLevels(preset.levels);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`${apiBasePath}/preset`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ preset: presetId }),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (res.ok) {
|
|
263
|
+
setSuccessMessage(`Applied ${preset.label} preset`);
|
|
264
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
265
|
+
fetchSettings();
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
// Preset applied locally even if API fails
|
|
269
|
+
console.error('Failed to apply preset:', err);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Save settings
|
|
274
|
+
const saveSettings = async () => {
|
|
275
|
+
if (!hasChanges || !editedLevels || !editedRetention || !editedLimits) return;
|
|
276
|
+
|
|
277
|
+
setSaving(true);
|
|
278
|
+
setError(null);
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const payload: Partial<LoggingSettings> = {
|
|
282
|
+
levels: editedLevels,
|
|
283
|
+
retention: editedRetention,
|
|
284
|
+
limits: editedLimits,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const res = await fetch(`${apiBasePath}/settings`, {
|
|
288
|
+
method: 'PUT',
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
body: JSON.stringify(payload),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (res.ok) {
|
|
294
|
+
setSuccessMessage('Settings saved successfully');
|
|
295
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
296
|
+
onSave?.(payload);
|
|
297
|
+
fetchSettings();
|
|
298
|
+
} else if (res.status === 404) {
|
|
299
|
+
// Endpoint not ready - simulate success
|
|
300
|
+
setSettings({
|
|
301
|
+
levels: editedLevels,
|
|
302
|
+
retention: editedRetention,
|
|
303
|
+
limits: editedLimits,
|
|
304
|
+
});
|
|
305
|
+
setSuccessMessage('Settings saved (demo mode)');
|
|
306
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
307
|
+
} else {
|
|
308
|
+
throw new Error('Failed to save settings');
|
|
309
|
+
}
|
|
310
|
+
} catch (err: any) {
|
|
311
|
+
setError(err.message);
|
|
312
|
+
} finally {
|
|
313
|
+
setSaving(false);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Manual prune
|
|
318
|
+
const handlePrune = async (option: string) => {
|
|
319
|
+
setPruning(true);
|
|
320
|
+
setError(null);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
let body: { older_than_days?: number; level?: string } = {};
|
|
324
|
+
|
|
325
|
+
switch (option) {
|
|
326
|
+
case 'debug_all':
|
|
327
|
+
body = { level: 'debug' };
|
|
328
|
+
break;
|
|
329
|
+
case 'older_7':
|
|
330
|
+
body = { older_than_days: 7 };
|
|
331
|
+
break;
|
|
332
|
+
case 'older_30':
|
|
333
|
+
body = { older_than_days: 30 };
|
|
334
|
+
break;
|
|
335
|
+
case 'all_keep_100':
|
|
336
|
+
body = { older_than_days: 0 }; // Server interprets as "all"
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const res = await fetch(`${apiBasePath}/prune`, {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: { 'Content-Type': 'application/json' },
|
|
343
|
+
body: JSON.stringify(body),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (res.ok) {
|
|
347
|
+
const data = await res.json();
|
|
348
|
+
setSuccessMessage(`Pruned ${data.deleted_rows?.toLocaleString() || 'some'} rows`);
|
|
349
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
350
|
+
fetchUsage();
|
|
351
|
+
} else if (res.status === 404) {
|
|
352
|
+
setSuccessMessage('Prune completed (demo mode)');
|
|
353
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
354
|
+
} else {
|
|
355
|
+
throw new Error('Failed to prune logs');
|
|
356
|
+
}
|
|
357
|
+
} catch (err: any) {
|
|
358
|
+
setError(err.message);
|
|
359
|
+
} finally {
|
|
360
|
+
setPruning(false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Get active preset
|
|
365
|
+
const getActivePreset = () => {
|
|
366
|
+
if (!editedLevels) return null;
|
|
367
|
+
return PRESETS.find(p => JSON.stringify(p.levels) === JSON.stringify(editedLevels))?.id || null;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
if (loading) {
|
|
371
|
+
return (
|
|
372
|
+
<div className="flex items-center justify-center py-12">
|
|
373
|
+
<RefreshCw className="w-8 h-8 animate-spin text-indigo-500" />
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div className="space-y-6">
|
|
380
|
+
{/* Error/Success Messages */}
|
|
381
|
+
{error && (
|
|
382
|
+
<div className={`flex items-center gap-3 p-4 rounded-lg ${isDark ? 'bg-red-900/30 border-red-700' : 'bg-red-50 border-red-300'} border`}>
|
|
383
|
+
<AlertCircle className={isDark ? 'text-red-400' : 'text-red-500'} size={20} />
|
|
384
|
+
<span className={isDark ? 'text-red-300' : 'text-red-700'}>{error}</span>
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{successMessage && (
|
|
389
|
+
<div className={`flex items-center gap-3 p-4 rounded-lg ${isDark ? 'bg-green-900/30 border-green-700' : 'bg-green-50 border-green-300'} border`}>
|
|
390
|
+
<Check className={isDark ? 'text-green-400' : 'text-green-500'} size={20} />
|
|
391
|
+
<span className={isDark ? 'text-green-300' : 'text-green-700'}>{successMessage}</span>
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
|
|
395
|
+
{/* Section 1: Environment Presets */}
|
|
396
|
+
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
397
|
+
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
398
|
+
<div className="flex items-center gap-2">
|
|
399
|
+
<Zap className={isDark ? 'text-amber-400' : 'text-amber-500'} size={20} />
|
|
400
|
+
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Quick Setup</h3>
|
|
401
|
+
</div>
|
|
402
|
+
<p className={`text-sm ${themeClasses.textMuted} mt-1`}>
|
|
403
|
+
Choose a preset to quickly configure log levels
|
|
404
|
+
</p>
|
|
405
|
+
</div>
|
|
406
|
+
<div className="p-5">
|
|
407
|
+
<div className="flex flex-wrap gap-3">
|
|
408
|
+
{PRESETS.map((preset) => {
|
|
409
|
+
const isActive = getActivePreset() === preset.id;
|
|
410
|
+
return (
|
|
411
|
+
<button
|
|
412
|
+
key={preset.id}
|
|
413
|
+
onClick={() => applyPreset(preset.id)}
|
|
414
|
+
className={`
|
|
415
|
+
px-4 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
416
|
+
${isActive
|
|
417
|
+
? 'bg-indigo-600 text-white ring-2 ring-indigo-500 ring-offset-2 ' + (isDark ? 'ring-offset-slate-800' : 'ring-offset-white')
|
|
418
|
+
: (isDark ? 'bg-slate-700 text-white hover:bg-slate-600' : 'bg-gray-100 text-gray-700 hover:bg-gray-200')
|
|
419
|
+
}
|
|
420
|
+
`}
|
|
421
|
+
>
|
|
422
|
+
{preset.label}
|
|
423
|
+
</button>
|
|
424
|
+
);
|
|
425
|
+
})}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{/* Section 2: Category Log Levels */}
|
|
431
|
+
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
432
|
+
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
433
|
+
<div className="flex items-center gap-2">
|
|
434
|
+
<Bug className={isDark ? 'text-violet-400' : 'text-violet-500'} size={20} />
|
|
435
|
+
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Log Levels</h3>
|
|
436
|
+
</div>
|
|
437
|
+
<p className={`text-sm ${themeClasses.textMuted} mt-1`}>
|
|
438
|
+
Set verbosity per category
|
|
439
|
+
</p>
|
|
440
|
+
</div>
|
|
441
|
+
<div className="p-5">
|
|
442
|
+
<div className="space-y-4">
|
|
443
|
+
{CATEGORIES.map((category) => (
|
|
444
|
+
<div key={category.id} className="flex items-center justify-between">
|
|
445
|
+
<div>
|
|
446
|
+
<p className={`font-medium ${themeClasses.textPrimary}`}>{category.label}</p>
|
|
447
|
+
<p className={`text-xs ${themeClasses.textMuted}`}>{category.description}</p>
|
|
448
|
+
</div>
|
|
449
|
+
<select
|
|
450
|
+
value={editedLevels?.[category.id] || 'info'}
|
|
451
|
+
onChange={(e) => {
|
|
452
|
+
setEditedLevels(prev => prev ? { ...prev, [category.id]: e.target.value as LogLevel } : null);
|
|
453
|
+
}}
|
|
454
|
+
className={`
|
|
455
|
+
px-3 py-2 rounded-lg text-sm font-medium cursor-pointer
|
|
456
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
457
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
458
|
+
`}
|
|
459
|
+
>
|
|
460
|
+
{LOG_LEVELS.map((level) => (
|
|
461
|
+
<option key={level.value} value={level.value}>
|
|
462
|
+
{level.label}
|
|
463
|
+
</option>
|
|
464
|
+
))}
|
|
465
|
+
</select>
|
|
466
|
+
</div>
|
|
467
|
+
))}
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
{/* Section 3: Retention Settings */}
|
|
473
|
+
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
474
|
+
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
475
|
+
<div className="flex items-center gap-2">
|
|
476
|
+
<Clock className={isDark ? 'text-cyan-400' : 'text-cyan-500'} size={20} />
|
|
477
|
+
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Retention Settings</h3>
|
|
478
|
+
</div>
|
|
479
|
+
<p className={`text-sm ${themeClasses.textMuted} mt-1`}>
|
|
480
|
+
How long to keep logs by severity level
|
|
481
|
+
</p>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="p-5">
|
|
484
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
|
485
|
+
{[
|
|
486
|
+
{ key: 'debug_days', label: 'Debug' },
|
|
487
|
+
{ key: 'info_days', label: 'Info' },
|
|
488
|
+
{ key: 'warn_days', label: 'Warnings' },
|
|
489
|
+
{ key: 'error_days', label: 'Errors' },
|
|
490
|
+
{ key: 'critical_days', label: 'Critical' },
|
|
491
|
+
].map((item) => (
|
|
492
|
+
<div key={item.key}>
|
|
493
|
+
<label className={`block text-sm ${themeClasses.textSecondary} mb-1`}>
|
|
494
|
+
{item.label}
|
|
495
|
+
</label>
|
|
496
|
+
<div className="flex items-center gap-2">
|
|
497
|
+
<input
|
|
498
|
+
type="number"
|
|
499
|
+
min="1"
|
|
500
|
+
max="365"
|
|
501
|
+
value={editedRetention?.[item.key as keyof RetentionSettings] || 7}
|
|
502
|
+
onChange={(e) => {
|
|
503
|
+
setEditedRetention(prev => prev ? { ...prev, [item.key]: parseInt(e.target.value) || 7 } : null);
|
|
504
|
+
}}
|
|
505
|
+
className={`
|
|
506
|
+
w-20 px-3 py-2 rounded-lg text-sm
|
|
507
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
508
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
509
|
+
`}
|
|
510
|
+
/>
|
|
511
|
+
<span className={`text-sm ${themeClasses.textMuted}`}>days</span>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
))}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
{/* Section 4: Storage & Pruning */}
|
|
520
|
+
<div className={`${themeClasses.cardBg} border rounded-xl overflow-hidden`}>
|
|
521
|
+
<div className={`px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
|
522
|
+
<div className="flex items-center gap-2">
|
|
523
|
+
<HardDrive className={isDark ? 'text-emerald-400' : 'text-emerald-500'} size={20} />
|
|
524
|
+
<h3 className={`font-semibold ${themeClasses.textPrimary}`}>Storage Limits</h3>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
<div className="p-5 space-y-6">
|
|
528
|
+
{/* Limit Inputs */}
|
|
529
|
+
<div className="flex flex-wrap items-center gap-6">
|
|
530
|
+
<div className="flex items-center gap-2">
|
|
531
|
+
<label className={`text-sm ${themeClasses.textSecondary}`}>Max size:</label>
|
|
532
|
+
<input
|
|
533
|
+
type="number"
|
|
534
|
+
min="1"
|
|
535
|
+
max="1000"
|
|
536
|
+
value={editedLimits?.max_size_mb || 10}
|
|
537
|
+
onChange={(e) => {
|
|
538
|
+
setEditedLimits(prev => prev ? { ...prev, max_size_mb: parseInt(e.target.value) || 10 } : null);
|
|
539
|
+
}}
|
|
540
|
+
className={`
|
|
541
|
+
w-20 px-3 py-2 rounded-lg text-sm
|
|
542
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
543
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
544
|
+
`}
|
|
545
|
+
/>
|
|
546
|
+
<span className={`text-sm ${themeClasses.textMuted}`}>MB</span>
|
|
547
|
+
</div>
|
|
548
|
+
<span className={themeClasses.textMuted}>or</span>
|
|
549
|
+
<div className="flex items-center gap-2">
|
|
550
|
+
<input
|
|
551
|
+
type="number"
|
|
552
|
+
min="100"
|
|
553
|
+
max="1000000"
|
|
554
|
+
step="1000"
|
|
555
|
+
value={editedLimits?.max_rows || 20000}
|
|
556
|
+
onChange={(e) => {
|
|
557
|
+
setEditedLimits(prev => prev ? { ...prev, max_rows: parseInt(e.target.value) || 20000 } : null);
|
|
558
|
+
}}
|
|
559
|
+
className={`
|
|
560
|
+
w-28 px-3 py-2 rounded-lg text-sm
|
|
561
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
562
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
563
|
+
`}
|
|
564
|
+
/>
|
|
565
|
+
<span className={`text-sm ${themeClasses.textMuted}`}>rows</span>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
{/* Usage Bar */}
|
|
570
|
+
{usage && (
|
|
571
|
+
<div>
|
|
572
|
+
<div className="flex items-center justify-between mb-2">
|
|
573
|
+
<span className={`text-sm ${themeClasses.textSecondary}`}>Current usage</span>
|
|
574
|
+
<span className={`text-sm font-medium ${themeClasses.textPrimary}`}>
|
|
575
|
+
{usage.total_size_mb?.toFixed(1) || 0} MB of {usage.limits.max_size_mb} MB ({usage.percent_used || 0}%)
|
|
576
|
+
</span>
|
|
577
|
+
</div>
|
|
578
|
+
<div className={`h-3 rounded-full overflow-hidden ${isDark ? 'bg-slate-700' : 'bg-gray-200'}`}>
|
|
579
|
+
<div
|
|
580
|
+
className={`h-full rounded-full transition-all ${
|
|
581
|
+
(usage.percent_used || 0) > 90 ? 'bg-red-500' :
|
|
582
|
+
(usage.percent_used || 0) > 70 ? 'bg-amber-500' :
|
|
583
|
+
'bg-emerald-500'
|
|
584
|
+
}`}
|
|
585
|
+
style={{ width: `${Math.min(usage.percent_used || 0, 100)}%` }}
|
|
586
|
+
/>
|
|
587
|
+
</div>
|
|
588
|
+
<div className={`flex items-center justify-between mt-2 text-xs ${themeClasses.textMuted}`}>
|
|
589
|
+
<span>{(usage.total_rows || 0).toLocaleString()} rows</span>
|
|
590
|
+
{usage.oldest_entry && (
|
|
591
|
+
<span>Oldest: {new Date(usage.oldest_entry).toLocaleDateString()}</span>
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
)}
|
|
596
|
+
|
|
597
|
+
{/* Prune Button */}
|
|
598
|
+
<div className="flex items-center gap-4">
|
|
599
|
+
<div className="relative">
|
|
600
|
+
<select
|
|
601
|
+
disabled={pruning}
|
|
602
|
+
onChange={(e) => {
|
|
603
|
+
if (e.target.value) {
|
|
604
|
+
handlePrune(e.target.value);
|
|
605
|
+
e.target.value = '';
|
|
606
|
+
}
|
|
607
|
+
}}
|
|
608
|
+
className={`
|
|
609
|
+
px-4 py-2.5 pr-10 rounded-lg text-sm font-medium cursor-pointer appearance-none
|
|
610
|
+
${isDark ? 'bg-red-900/30 text-red-300 border-red-700 hover:bg-red-900/50' : 'bg-red-50 text-red-700 border-red-300 hover:bg-red-100'}
|
|
611
|
+
border focus:outline-none focus:ring-2 focus:ring-red-500
|
|
612
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
613
|
+
`}
|
|
614
|
+
>
|
|
615
|
+
<option value="">Prune Now...</option>
|
|
616
|
+
{PRUNE_OPTIONS.map((opt) => (
|
|
617
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
618
|
+
))}
|
|
619
|
+
</select>
|
|
620
|
+
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${isDark ? 'text-red-400' : 'text-red-500'}`} />
|
|
621
|
+
</div>
|
|
622
|
+
{pruning && (
|
|
623
|
+
<span className={`flex items-center gap-2 text-sm ${themeClasses.textMuted}`}>
|
|
624
|
+
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
625
|
+
Pruning...
|
|
626
|
+
</span>
|
|
627
|
+
)}
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
{/* Save Button */}
|
|
633
|
+
<div className="flex items-center justify-end gap-4">
|
|
634
|
+
{hasChanges && (
|
|
635
|
+
<span className={`text-sm ${themeClasses.textMuted}`}>
|
|
636
|
+
You have unsaved changes
|
|
637
|
+
</span>
|
|
638
|
+
)}
|
|
639
|
+
<button
|
|
640
|
+
onClick={saveSettings}
|
|
641
|
+
disabled={!hasChanges || saving}
|
|
642
|
+
className={`
|
|
643
|
+
px-6 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
644
|
+
${hasChanges
|
|
645
|
+
? 'bg-indigo-600 text-white hover:bg-indigo-700'
|
|
646
|
+
: (isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-400')
|
|
647
|
+
}
|
|
648
|
+
disabled:cursor-not-allowed
|
|
649
|
+
`}
|
|
650
|
+
>
|
|
651
|
+
{saving ? (
|
|
652
|
+
<span className="flex items-center gap-2">
|
|
653
|
+
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
654
|
+
Saving...
|
|
655
|
+
</span>
|
|
656
|
+
) : (
|
|
657
|
+
'Save Settings'
|
|
658
|
+
)}
|
|
659
|
+
</button>
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export default LoggingSettingsTab;
|