Stijnus
commited on
Commit
·
a94330e
1
Parent(s):
6d98aff
fixes
Browse files- app/components/settings/CHANGELOG.md +189 -0
- app/components/settings/connections/ConnectionsTab.tsx +1 -1
- app/components/settings/debug/DebugTab.tsx +152 -145
- app/components/settings/event-logs/EventLogsTab.tsx +333 -427
- app/components/settings/task-manager/TaskManagerTab.tsx +59 -33
- app/components/settings/update/UpdateTab.tsx +1 -1
- app/lib/stores/logs.ts +95 -1
- app/routes/api.system.app-info.ts +45 -0
- changelogUI.md +0 -214
app/components/settings/CHANGELOG.md
ADDED
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Settings Components Changelog
|
2 |
+
|
3 |
+
All notable changes to the settings components will be documented in this file.
|
4 |
+
|
5 |
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6 |
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7 |
+
|
8 |
+
## [Unreleased]
|
9 |
+
|
10 |
+
### Added
|
11 |
+
|
12 |
+
- New Settings Dashboard with improved UI/UX
|
13 |
+
- Tab management system with drag-and-drop reordering
|
14 |
+
- Enhanced developer tools window
|
15 |
+
- Bulk update functionality for Ollama models
|
16 |
+
- System performance monitoring in Debug tab
|
17 |
+
- Data import/export functionality
|
18 |
+
- Enhanced event logging system
|
19 |
+
- Profile customization options
|
20 |
+
- Auto energy saver mode in TaskManager
|
21 |
+
- Energy savings tracking and statistics
|
22 |
+
- Persistent settings storage using localStorage
|
23 |
+
|
24 |
+
### Changed
|
25 |
+
|
26 |
+
- Removed green status indicators from TaskManagerTab for cleaner UI
|
27 |
+
- Changed connection status indicators to use neutral colors
|
28 |
+
- Updated energy saver mode indicator to use neutral colors
|
29 |
+
- Simplified process status display in TaskManager
|
30 |
+
- Improved tab organization with window-specific grouping
|
31 |
+
- Enhanced settings persistence with better localStorage handling
|
32 |
+
|
33 |
+
### Fixed
|
34 |
+
|
35 |
+
- Status indicator consistency across dark/light themes
|
36 |
+
- Process status updates during energy saver mode
|
37 |
+
- UI rendering issues in dark mode
|
38 |
+
- Tab visibility state management
|
39 |
+
- Settings import/export reliability
|
40 |
+
|
41 |
+
## [1.0.0] - Initial Release
|
42 |
+
|
43 |
+
### Added
|
44 |
+
|
45 |
+
#### User Window Components
|
46 |
+
|
47 |
+
- **Profile Tab**
|
48 |
+
|
49 |
+
- User profile and account settings management
|
50 |
+
- Avatar customization
|
51 |
+
- Account preferences
|
52 |
+
|
53 |
+
- **Settings Tab**
|
54 |
+
|
55 |
+
- Application preferences configuration
|
56 |
+
- UI behavior customization
|
57 |
+
- General settings management
|
58 |
+
|
59 |
+
- **Notifications Tab**
|
60 |
+
|
61 |
+
- Real-time notification center
|
62 |
+
- Unread notification tracking
|
63 |
+
- Notification preferences
|
64 |
+
- Support for different notification types
|
65 |
+
- Integration with logStore
|
66 |
+
|
67 |
+
- **Cloud Providers Tab**
|
68 |
+
|
69 |
+
- Cloud-based AI provider configuration
|
70 |
+
- API key management
|
71 |
+
- Cloud model selection
|
72 |
+
- Provider-specific settings
|
73 |
+
- Status monitoring
|
74 |
+
|
75 |
+
- **Local Providers Tab**
|
76 |
+
|
77 |
+
- Local AI model management
|
78 |
+
- Ollama integration and model updates
|
79 |
+
- LM Studio configuration
|
80 |
+
- Local inference settings
|
81 |
+
- Model download and updates
|
82 |
+
|
83 |
+
- **Task Manager Tab**
|
84 |
+
|
85 |
+
- System resource monitoring
|
86 |
+
- Process management
|
87 |
+
- Performance metrics and graphs
|
88 |
+
- Battery status monitoring
|
89 |
+
- Energy saving features
|
90 |
+
- Alert configurations
|
91 |
+
|
92 |
+
- **Connections Tab**
|
93 |
+
|
94 |
+
- Network status monitoring
|
95 |
+
- GitHub integration
|
96 |
+
- Connection health metrics
|
97 |
+
- Secure token storage
|
98 |
+
- Auto-reconnect settings
|
99 |
+
|
100 |
+
- **Debug Tab**
|
101 |
+
|
102 |
+
- System diagnostics
|
103 |
+
- Performance monitoring
|
104 |
+
- Error tracking
|
105 |
+
- Provider status checks
|
106 |
+
|
107 |
+
- **Event Logs Tab**
|
108 |
+
|
109 |
+
- Comprehensive system logs
|
110 |
+
- Filtered log views
|
111 |
+
- Log management tools
|
112 |
+
- Error tracking
|
113 |
+
- Performance metrics
|
114 |
+
|
115 |
+
- **Update Tab**
|
116 |
+
- Version management
|
117 |
+
- Update notifications
|
118 |
+
- Release notes
|
119 |
+
- Auto-update configuration
|
120 |
+
|
121 |
+
### Technical Enhancements
|
122 |
+
|
123 |
+
#### State Management
|
124 |
+
|
125 |
+
- Implemented Nano Stores for efficient state handling
|
126 |
+
- Added persistent settings storage
|
127 |
+
- Real-time state synchronization
|
128 |
+
- Provider state management
|
129 |
+
|
130 |
+
#### Performance
|
131 |
+
|
132 |
+
- Lazy loading of tab contents
|
133 |
+
- Efficient DOM updates
|
134 |
+
- Optimized animations
|
135 |
+
- Resource monitoring
|
136 |
+
|
137 |
+
#### Accessibility
|
138 |
+
|
139 |
+
- Keyboard navigation support
|
140 |
+
- Screen reader compatibility
|
141 |
+
- Focus management
|
142 |
+
- ARIA attributes implementation
|
143 |
+
|
144 |
+
#### UI/UX Features
|
145 |
+
|
146 |
+
- Drag & Drop tab management
|
147 |
+
- Dynamic status updates
|
148 |
+
- Responsive design with Framer Motion
|
149 |
+
- Dark/Light mode support
|
150 |
+
- Enhanced provider management
|
151 |
+
- Resource monitoring dashboard
|
152 |
+
|
153 |
+
### Dependencies
|
154 |
+
|
155 |
+
- Radix UI for accessible components
|
156 |
+
- Framer Motion for animations
|
157 |
+
- React DnD for drag and drop
|
158 |
+
- Nano Stores for state management
|
159 |
+
|
160 |
+
## Future Plans
|
161 |
+
|
162 |
+
- Additional customization options
|
163 |
+
- Enhanced theme support
|
164 |
+
- Extended API integrations
|
165 |
+
- Advanced monitoring capabilities
|
166 |
+
- Custom provider plugins
|
167 |
+
- Enhanced resource management
|
168 |
+
- Advanced debugging features
|
169 |
+
|
170 |
+
## Historical Changes
|
171 |
+
|
172 |
+
### Task Manager
|
173 |
+
|
174 |
+
- Added real-time system metrics monitoring
|
175 |
+
- Implemented process tracking functionality
|
176 |
+
- Added battery status monitoring
|
177 |
+
- Integrated energy saving features
|
178 |
+
|
179 |
+
### Connections
|
180 |
+
|
181 |
+
- Added GitHub integration
|
182 |
+
- Implemented secure token storage
|
183 |
+
- Added connection status indicators
|
184 |
+
|
185 |
+
### Notifications
|
186 |
+
|
187 |
+
- Implemented centralized notification system
|
188 |
+
- Added support for different notification types (error, warning, update)
|
189 |
+
- Integrated with logStore for persistent storage
|
app/components/settings/connections/ConnectionsTab.tsx
CHANGED
@@ -194,7 +194,7 @@ export default function ConnectionsTab() {
|
|
194 |
)}
|
195 |
|
196 |
{connection.user && (
|
197 |
-
<span className="text-sm text-
|
198 |
<div className="i-ph:check-circle w-4 h-4" />
|
199 |
Connected to GitHub
|
200 |
</span>
|
|
|
194 |
)}
|
195 |
|
196 |
{connection.user && (
|
197 |
+
<span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
|
198 |
<div className="i-ph:check-circle w-4 h-4" />
|
199 |
Connected to GitHub
|
200 |
</span>
|
app/components/settings/debug/DebugTab.tsx
CHANGED
@@ -4,13 +4,6 @@ import { classNames } from '~/utils/classNames';
|
|
4 |
import { logStore } from '~/lib/stores/logs';
|
5 |
import type { LogEntry } from '~/lib/stores/logs';
|
6 |
|
7 |
-
interface ProviderStatus {
|
8 |
-
id: string;
|
9 |
-
name: string;
|
10 |
-
status: 'online' | 'offline' | 'error';
|
11 |
-
error?: string;
|
12 |
-
}
|
13 |
-
|
14 |
interface SystemInfo {
|
15 |
os: string;
|
16 |
arch: string;
|
@@ -90,14 +83,24 @@ interface SystemInfo {
|
|
90 |
};
|
91 |
}
|
92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
export default function DebugTab() {
|
94 |
-
const [providerStatuses, setProviderStatuses] = useState<ProviderStatus[]>([]);
|
95 |
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
|
|
96 |
const [loading, setLoading] = useState({
|
97 |
systemInfo: false,
|
98 |
-
providers: false,
|
99 |
performance: false,
|
100 |
errors: false,
|
|
|
101 |
});
|
102 |
const [errorLog, setErrorLog] = useState<{
|
103 |
errors: any[];
|
@@ -109,8 +112,8 @@ export default function DebugTab() {
|
|
109 |
|
110 |
// Fetch initial data
|
111 |
useEffect(() => {
|
112 |
-
checkProviderStatus();
|
113 |
getSystemInfo();
|
|
|
114 |
}, []);
|
115 |
|
116 |
// Set up error listeners when component mounts
|
@@ -146,75 +149,6 @@ export default function DebugTab() {
|
|
146 |
};
|
147 |
}, []);
|
148 |
|
149 |
-
const checkProviderStatus = async () => {
|
150 |
-
try {
|
151 |
-
setLoading((prev) => ({ ...prev, providers: true }));
|
152 |
-
|
153 |
-
// Fetch real provider statuses
|
154 |
-
const providers: ProviderStatus[] = [];
|
155 |
-
|
156 |
-
// Check OpenAI status
|
157 |
-
try {
|
158 |
-
const openaiResponse = await fetch('/api/providers/openai/status');
|
159 |
-
providers.push({
|
160 |
-
id: 'openai',
|
161 |
-
name: 'OpenAI',
|
162 |
-
status: openaiResponse.ok ? 'online' : 'error',
|
163 |
-
error: !openaiResponse.ok ? 'API Error' : undefined,
|
164 |
-
});
|
165 |
-
} catch {
|
166 |
-
providers.push({ id: 'openai', name: 'OpenAI', status: 'offline' });
|
167 |
-
}
|
168 |
-
|
169 |
-
// Check Anthropic status
|
170 |
-
try {
|
171 |
-
const anthropicResponse = await fetch('/api/providers/anthropic/status');
|
172 |
-
providers.push({
|
173 |
-
id: 'anthropic',
|
174 |
-
name: 'Anthropic',
|
175 |
-
status: anthropicResponse.ok ? 'online' : 'error',
|
176 |
-
error: !anthropicResponse.ok ? 'API Error' : undefined,
|
177 |
-
});
|
178 |
-
} catch {
|
179 |
-
providers.push({ id: 'anthropic', name: 'Anthropic', status: 'offline' });
|
180 |
-
}
|
181 |
-
|
182 |
-
// Check Local Models status
|
183 |
-
try {
|
184 |
-
const localResponse = await fetch('/api/providers/local/status');
|
185 |
-
providers.push({
|
186 |
-
id: 'local',
|
187 |
-
name: 'Local Models',
|
188 |
-
status: localResponse.ok ? 'online' : 'error',
|
189 |
-
error: !localResponse.ok ? 'API Error' : undefined,
|
190 |
-
});
|
191 |
-
} catch {
|
192 |
-
providers.push({ id: 'local', name: 'Local Models', status: 'offline' });
|
193 |
-
}
|
194 |
-
|
195 |
-
// Check Ollama status
|
196 |
-
try {
|
197 |
-
const ollamaResponse = await fetch('/api/providers/ollama/status');
|
198 |
-
providers.push({
|
199 |
-
id: 'ollama',
|
200 |
-
name: 'Ollama',
|
201 |
-
status: ollamaResponse.ok ? 'online' : 'error',
|
202 |
-
error: !ollamaResponse.ok ? 'API Error' : undefined,
|
203 |
-
});
|
204 |
-
} catch {
|
205 |
-
providers.push({ id: 'ollama', name: 'Ollama', status: 'offline' });
|
206 |
-
}
|
207 |
-
|
208 |
-
setProviderStatuses(providers);
|
209 |
-
toast.success('Provider status updated');
|
210 |
-
} catch (error) {
|
211 |
-
toast.error('Failed to check provider status');
|
212 |
-
console.error('Failed to check provider status:', error);
|
213 |
-
} finally {
|
214 |
-
setLoading((prev) => ({ ...prev, providers: false }));
|
215 |
-
}
|
216 |
-
};
|
217 |
-
|
218 |
const getSystemInfo = async () => {
|
219 |
try {
|
220 |
setLoading((prev) => ({ ...prev, systemInfo: true }));
|
@@ -360,6 +294,26 @@ export default function DebugTab() {
|
|
360 |
}
|
361 |
};
|
362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
363 |
// Helper function to format bytes to human readable format
|
364 |
const formatBytes = (bytes: number) => {
|
365 |
const units = ['B', 'KB', 'MB', 'GB'];
|
@@ -509,29 +463,40 @@ export default function DebugTab() {
|
|
509 |
}
|
510 |
};
|
511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
512 |
return (
|
513 |
<div className="flex flex-col gap-6">
|
514 |
{/* Action Buttons */}
|
515 |
<div className="flex flex-wrap gap-4">
|
516 |
-
<button
|
517 |
-
onClick={checkProviderStatus}
|
518 |
-
disabled={loading.providers}
|
519 |
-
className={classNames(
|
520 |
-
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
521 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
522 |
-
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
523 |
-
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
524 |
-
{ 'opacity-50 cursor-not-allowed': loading.providers },
|
525 |
-
)}
|
526 |
-
>
|
527 |
-
{loading.providers ? (
|
528 |
-
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
529 |
-
) : (
|
530 |
-
<div className="i-ph:plug w-4 h-4" />
|
531 |
-
)}
|
532 |
-
Check Providers
|
533 |
-
</button>
|
534 |
-
|
535 |
<button
|
536 |
onClick={getSystemInfo}
|
537 |
disabled={loading.systemInfo}
|
@@ -588,6 +553,19 @@ export default function DebugTab() {
|
|
588 |
)}
|
589 |
Check Errors
|
590 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
591 |
</div>
|
592 |
|
593 |
{/* Error Log Display */}
|
@@ -772,53 +750,6 @@ export default function DebugTab() {
|
|
772 |
)}
|
773 |
</div>
|
774 |
|
775 |
-
{/* Provider Status */}
|
776 |
-
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
777 |
-
<div className="flex items-center justify-between mb-4">
|
778 |
-
<div className="flex items-center gap-3">
|
779 |
-
<div className="i-ph:robot text-purple-500 w-5 h-5" />
|
780 |
-
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Provider Status</h3>
|
781 |
-
</div>
|
782 |
-
<button
|
783 |
-
onClick={checkProviderStatus}
|
784 |
-
className={classNames(
|
785 |
-
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
786 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
787 |
-
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
788 |
-
'transition-colors duration-200',
|
789 |
-
{ 'opacity-50 cursor-not-allowed': loading.providers },
|
790 |
-
)}
|
791 |
-
disabled={loading.providers}
|
792 |
-
>
|
793 |
-
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
|
794 |
-
Refresh
|
795 |
-
</button>
|
796 |
-
</div>
|
797 |
-
<div className="grid grid-cols-2 gap-4">
|
798 |
-
{providerStatuses.map((provider) => (
|
799 |
-
<div
|
800 |
-
key={provider.id}
|
801 |
-
className="flex items-center justify-between p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]"
|
802 |
-
>
|
803 |
-
<div className="flex items-center gap-3">
|
804 |
-
<div
|
805 |
-
className={classNames(
|
806 |
-
'w-2 h-2 rounded-full',
|
807 |
-
provider.status === 'online'
|
808 |
-
? 'bg-green-500'
|
809 |
-
: provider.status === 'offline'
|
810 |
-
? 'bg-red-500'
|
811 |
-
: 'bg-yellow-500',
|
812 |
-
)}
|
813 |
-
/>
|
814 |
-
<span className="text-sm text-bolt-elements-textPrimary">{provider.name}</span>
|
815 |
-
</div>
|
816 |
-
<span className="text-xs text-bolt-elements-textSecondary capitalize">{provider.status}</span>
|
817 |
-
</div>
|
818 |
-
))}
|
819 |
-
</div>
|
820 |
-
</div>
|
821 |
-
|
822 |
{/* Performance Metrics */}
|
823 |
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
824 |
<div className="flex items-center justify-between mb-4">
|
@@ -906,6 +837,82 @@ export default function DebugTab() {
|
|
906 |
)}
|
907 |
</div>
|
908 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
909 |
{/* Error Check */}
|
910 |
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
911 |
<div className="flex items-center justify-between mb-4">
|
|
|
4 |
import { logStore } from '~/lib/stores/logs';
|
5 |
import type { LogEntry } from '~/lib/stores/logs';
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
interface SystemInfo {
|
8 |
os: string;
|
9 |
arch: string;
|
|
|
83 |
};
|
84 |
}
|
85 |
|
86 |
+
interface WebAppInfo {
|
87 |
+
name: string;
|
88 |
+
version: string;
|
89 |
+
description: string;
|
90 |
+
license: string;
|
91 |
+
nodeVersion: string;
|
92 |
+
dependencies: { [key: string]: string };
|
93 |
+
devDependencies: { [key: string]: string };
|
94 |
+
}
|
95 |
+
|
96 |
export default function DebugTab() {
|
|
|
97 |
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
98 |
+
const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
|
99 |
const [loading, setLoading] = useState({
|
100 |
systemInfo: false,
|
|
|
101 |
performance: false,
|
102 |
errors: false,
|
103 |
+
webAppInfo: false,
|
104 |
});
|
105 |
const [errorLog, setErrorLog] = useState<{
|
106 |
errors: any[];
|
|
|
112 |
|
113 |
// Fetch initial data
|
114 |
useEffect(() => {
|
|
|
115 |
getSystemInfo();
|
116 |
+
getWebAppInfo();
|
117 |
}, []);
|
118 |
|
119 |
// Set up error listeners when component mounts
|
|
|
149 |
};
|
150 |
}, []);
|
151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
const getSystemInfo = async () => {
|
153 |
try {
|
154 |
setLoading((prev) => ({ ...prev, systemInfo: true }));
|
|
|
294 |
}
|
295 |
};
|
296 |
|
297 |
+
const getWebAppInfo = async () => {
|
298 |
+
try {
|
299 |
+
setLoading((prev) => ({ ...prev, webAppInfo: true }));
|
300 |
+
|
301 |
+
const response = await fetch('/api/system/app-info');
|
302 |
+
|
303 |
+
if (!response.ok) {
|
304 |
+
throw new Error('Failed to fetch webapp info');
|
305 |
+
}
|
306 |
+
|
307 |
+
const data = await response.json();
|
308 |
+
setWebAppInfo(data as WebAppInfo);
|
309 |
+
} catch (error) {
|
310 |
+
console.error('Failed to fetch webapp info:', error);
|
311 |
+
toast.error('Failed to fetch webapp information');
|
312 |
+
} finally {
|
313 |
+
setLoading((prev) => ({ ...prev, webAppInfo: false }));
|
314 |
+
}
|
315 |
+
};
|
316 |
+
|
317 |
// Helper function to format bytes to human readable format
|
318 |
const formatBytes = (bytes: number) => {
|
319 |
const units = ['B', 'KB', 'MB', 'GB'];
|
|
|
463 |
}
|
464 |
};
|
465 |
|
466 |
+
const exportDebugInfo = () => {
|
467 |
+
try {
|
468 |
+
const debugData = {
|
469 |
+
timestamp: new Date().toISOString(),
|
470 |
+
system: systemInfo,
|
471 |
+
webApp: webAppInfo,
|
472 |
+
errors: errorLog.errors,
|
473 |
+
performance: {
|
474 |
+
memory: (performance as any).memory || {},
|
475 |
+
timing: performance.timing,
|
476 |
+
navigation: performance.navigation,
|
477 |
+
},
|
478 |
+
};
|
479 |
+
|
480 |
+
const blob = new Blob([JSON.stringify(debugData, null, 2)], { type: 'application/json' });
|
481 |
+
const url = window.URL.createObjectURL(blob);
|
482 |
+
const a = document.createElement('a');
|
483 |
+
a.href = url;
|
484 |
+
a.download = `bolt-debug-info-${new Date().toISOString()}.json`;
|
485 |
+
document.body.appendChild(a);
|
486 |
+
a.click();
|
487 |
+
window.URL.revokeObjectURL(url);
|
488 |
+
document.body.removeChild(a);
|
489 |
+
toast.success('Debug information exported successfully');
|
490 |
+
} catch (error) {
|
491 |
+
console.error('Failed to export debug info:', error);
|
492 |
+
toast.error('Failed to export debug information');
|
493 |
+
}
|
494 |
+
};
|
495 |
+
|
496 |
return (
|
497 |
<div className="flex flex-col gap-6">
|
498 |
{/* Action Buttons */}
|
499 |
<div className="flex flex-wrap gap-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
<button
|
501 |
onClick={getSystemInfo}
|
502 |
disabled={loading.systemInfo}
|
|
|
553 |
)}
|
554 |
Check Errors
|
555 |
</button>
|
556 |
+
|
557 |
+
<button
|
558 |
+
onClick={exportDebugInfo}
|
559 |
+
className={classNames(
|
560 |
+
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
561 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
562 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
563 |
+
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
564 |
+
)}
|
565 |
+
>
|
566 |
+
<div className="i-ph:download w-4 h-4" />
|
567 |
+
Export Debug Info
|
568 |
+
</button>
|
569 |
</div>
|
570 |
|
571 |
{/* Error Log Display */}
|
|
|
750 |
)}
|
751 |
</div>
|
752 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
753 |
{/* Performance Metrics */}
|
754 |
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
755 |
<div className="flex items-center justify-between mb-4">
|
|
|
837 |
)}
|
838 |
</div>
|
839 |
|
840 |
+
{/* WebApp Information */}
|
841 |
+
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
842 |
+
<div className="flex items-center justify-between mb-4">
|
843 |
+
<div className="flex items-center gap-3">
|
844 |
+
<div className="i-ph:info text-blue-500 w-5 h-5" />
|
845 |
+
<h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
|
846 |
+
</div>
|
847 |
+
<button
|
848 |
+
onClick={getWebAppInfo}
|
849 |
+
className={classNames(
|
850 |
+
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
851 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
852 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
853 |
+
'transition-colors duration-200',
|
854 |
+
{ 'opacity-50 cursor-not-allowed': loading.webAppInfo },
|
855 |
+
)}
|
856 |
+
disabled={loading.webAppInfo}
|
857 |
+
>
|
858 |
+
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.webAppInfo ? 'animate-spin' : '')} />
|
859 |
+
Refresh
|
860 |
+
</button>
|
861 |
+
</div>
|
862 |
+
{webAppInfo ? (
|
863 |
+
<div className="grid grid-cols-2 gap-4">
|
864 |
+
<div className="space-y-2">
|
865 |
+
<div className="text-sm flex items-center gap-2">
|
866 |
+
<div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
|
867 |
+
<span className="text-bolt-elements-textSecondary">Name: </span>
|
868 |
+
<span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
|
869 |
+
</div>
|
870 |
+
<div className="text-sm flex items-center gap-2">
|
871 |
+
<div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
|
872 |
+
<span className="text-bolt-elements-textSecondary">Version: </span>
|
873 |
+
<span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
|
874 |
+
</div>
|
875 |
+
<div className="text-sm flex items-center gap-2">
|
876 |
+
<div className="i-ph:file-text text-bolt-elements-textSecondary w-4 h-4" />
|
877 |
+
<span className="text-bolt-elements-textSecondary">Description: </span>
|
878 |
+
<span className="text-bolt-elements-textPrimary">{webAppInfo.description}</span>
|
879 |
+
</div>
|
880 |
+
<div className="text-sm flex items-center gap-2">
|
881 |
+
<div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
|
882 |
+
<span className="text-bolt-elements-textSecondary">License: </span>
|
883 |
+
<span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
|
884 |
+
</div>
|
885 |
+
<div className="text-sm flex items-center gap-2">
|
886 |
+
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
|
887 |
+
<span className="text-bolt-elements-textSecondary">Node Version: </span>
|
888 |
+
<span className="text-bolt-elements-textPrimary">{webAppInfo.nodeVersion}</span>
|
889 |
+
</div>
|
890 |
+
</div>
|
891 |
+
<div className="space-y-2">
|
892 |
+
<div className="text-sm">
|
893 |
+
<div className="flex items-center gap-2 mb-2">
|
894 |
+
<div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
|
895 |
+
<span className="text-bolt-elements-textSecondary">Key Dependencies:</span>
|
896 |
+
</div>
|
897 |
+
<div className="pl-6 space-y-1">
|
898 |
+
{Object.entries(webAppInfo.dependencies)
|
899 |
+
.filter(([key]) => ['react', '@remix-run/react', 'next', 'typescript'].includes(key))
|
900 |
+
.map(([key, version]) => (
|
901 |
+
<div key={key} className="text-xs text-bolt-elements-textPrimary">
|
902 |
+
{key}: {version}
|
903 |
+
</div>
|
904 |
+
))}
|
905 |
+
</div>
|
906 |
+
</div>
|
907 |
+
</div>
|
908 |
+
</div>
|
909 |
+
) : (
|
910 |
+
<div className="text-sm text-bolt-elements-textSecondary">
|
911 |
+
{loading.webAppInfo ? 'Loading webapp information...' : 'No webapp information available'}
|
912 |
+
</div>
|
913 |
+
)}
|
914 |
+
</div>
|
915 |
+
|
916 |
{/* Error Check */}
|
917 |
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
918 |
<div className="flex items-center justify-between mb-4">
|
app/components/settings/event-logs/EventLogsTab.tsx
CHANGED
@@ -1,515 +1,421 @@
|
|
1 |
-
import React, { useCallback, useEffect,
|
2 |
-
import {
|
3 |
import { Switch } from '~/components/ui/Switch';
|
4 |
import { logStore, type LogEntry } from '~/lib/stores/logs';
|
5 |
import { useStore } from '@nanostores/react';
|
6 |
import { classNames } from '~/utils/classNames';
|
7 |
-
import { motion } from 'framer-motion';
|
8 |
|
9 |
interface SelectOption {
|
10 |
value: string;
|
11 |
label: string;
|
12 |
-
icon
|
13 |
color?: string;
|
14 |
}
|
15 |
|
16 |
const logLevelOptions: SelectOption[] = [
|
17 |
-
{
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
];
|
23 |
|
24 |
const logCategoryOptions: SelectOption[] = [
|
25 |
-
{
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
];
|
31 |
|
32 |
-
|
33 |
-
value,
|
34 |
-
onChange,
|
35 |
-
options,
|
36 |
-
className,
|
37 |
-
}: {
|
38 |
-
value: string;
|
39 |
-
onChange: (value: string) => void;
|
40 |
-
options: SelectOption[];
|
41 |
-
className?: string;
|
42 |
-
}) => {
|
43 |
-
const [isExpanded, setIsExpanded] = useState(false);
|
44 |
-
const selectedOption = options.find((opt) => opt.value === value);
|
45 |
-
|
46 |
-
if (!isExpanded) {
|
47 |
-
return (
|
48 |
-
<button
|
49 |
-
type="button"
|
50 |
-
onClick={() => setIsExpanded(true)}
|
51 |
-
className={classNames(
|
52 |
-
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm',
|
53 |
-
'bg-white/50 dark:bg-gray-800/30',
|
54 |
-
'hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
55 |
-
'border border-gray-200/50 dark:border-gray-700/50',
|
56 |
-
'text-bolt-elements-textPrimary',
|
57 |
-
className,
|
58 |
-
)}
|
59 |
-
>
|
60 |
-
<div className={classNames(selectedOption?.icon, 'text-base text-purple-500')} />
|
61 |
-
<span className="text-sm">{selectedOption?.label}</span>
|
62 |
-
<div className="i-ph:caret-right text-sm text-bolt-elements-textTertiary" />
|
63 |
-
</button>
|
64 |
-
);
|
65 |
-
}
|
66 |
-
|
67 |
-
return (
|
68 |
-
<div className="flex items-center gap-0.5 p-0.5 rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50">
|
69 |
-
{options.map((option) => (
|
70 |
-
<button
|
71 |
-
key={option.value}
|
72 |
-
type="button"
|
73 |
-
onClick={() => {
|
74 |
-
onChange(option.value);
|
75 |
-
setIsExpanded(false);
|
76 |
-
}}
|
77 |
-
className={classNames(
|
78 |
-
'flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-colors',
|
79 |
-
option.value === value
|
80 |
-
? 'bg-purple-100 dark:bg-purple-800/40 text-purple-900 dark:text-purple-200'
|
81 |
-
: 'text-bolt-elements-textSecondary hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
82 |
-
)}
|
83 |
-
>
|
84 |
-
<div className={classNames(option.icon, 'text-base', option.value === value ? option.color : '')} />
|
85 |
-
<span className="truncate">{option.label}</span>
|
86 |
-
</button>
|
87 |
-
))}
|
88 |
-
</div>
|
89 |
-
);
|
90 |
-
};
|
91 |
-
|
92 |
-
const LogEntryItem = ({
|
93 |
-
log,
|
94 |
-
isExpanded: forceExpanded,
|
95 |
-
use24Hour,
|
96 |
-
}: {
|
97 |
log: LogEntry;
|
98 |
isExpanded: boolean;
|
99 |
use24Hour: boolean;
|
100 |
-
|
101 |
-
|
102 |
-
const [isCopied, setIsCopied] = useState(false);
|
103 |
|
|
|
|
|
|
|
|
|
104 |
useEffect(() => {
|
105 |
-
|
106 |
}, [forceExpanded]);
|
107 |
|
108 |
-
const
|
109 |
-
const logText = `[${log.level.toUpperCase()}] ${log.message}\nTimestamp: ${new Date(
|
110 |
-
log.timestamp,
|
111 |
-
).toLocaleString()}\nCategory: ${log.category}\nDetails: ${JSON.stringify(log.details, null, 2)}`;
|
112 |
-
|
113 |
-
navigator.clipboard.writeText(logText).then(() => {
|
114 |
-
setIsCopied(true);
|
115 |
-
toast.success('Log copied to clipboard');
|
116 |
-
setTimeout(() => setIsCopied(false), 2000);
|
117 |
-
});
|
118 |
-
}, [log]);
|
119 |
-
|
120 |
-
const formattedTime = useMemo(() => {
|
121 |
const date = new Date(log.timestamp);
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
const timeStr = date.toLocaleTimeString(undefined, {
|
127 |
-
hour: '2-digit',
|
128 |
-
minute: '2-digit',
|
129 |
-
hour12: !use24Hour,
|
130 |
-
});
|
131 |
-
|
132 |
-
if (isToday) {
|
133 |
-
return {
|
134 |
-
primary: timeStr,
|
135 |
-
secondary: 'Today',
|
136 |
-
};
|
137 |
-
} else if (isYesterday) {
|
138 |
-
return {
|
139 |
-
primary: timeStr,
|
140 |
-
secondary: 'Yesterday',
|
141 |
-
};
|
142 |
-
} else {
|
143 |
-
const dateStr = date.toLocaleDateString(undefined, {
|
144 |
-
month: 'short',
|
145 |
-
day: 'numeric',
|
146 |
-
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
|
147 |
-
});
|
148 |
-
return {
|
149 |
-
primary: dateStr,
|
150 |
-
secondary: timeStr,
|
151 |
-
};
|
152 |
}
|
|
|
|
|
153 |
}, [log.timestamp, use24Hour]);
|
154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
return (
|
156 |
-
<div
|
157 |
-
className=
|
158 |
-
'
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
'bg-red-100/80 text-red-800 dark:bg-red-500/10 dark:text-red-400': log.level === 'error',
|
169 |
-
'bg-yellow-100/80 text-yellow-800 dark:bg-yellow-500/10 dark:text-yellow-400': log.level === 'warning',
|
170 |
-
'bg-blue-100/80 text-blue-800 dark:bg-blue-500/10 dark:text-blue-400': log.level === 'info',
|
171 |
-
'bg-gray-100/80 text-gray-800 dark:bg-gray-500/10 dark:text-gray-400': log.level === 'debug',
|
172 |
-
})}
|
173 |
-
>
|
174 |
-
{log.level}
|
175 |
-
</span>
|
176 |
-
<p className="flex-1 text-sm text-bolt-elements-textPrimary">{log.message}</p>
|
177 |
-
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
178 |
-
<button onClick={handleCopy} className="p-1 transition-colors rounded focus:outline-none" title="Copy log">
|
179 |
-
<div
|
180 |
-
className={classNames(
|
181 |
-
'text-base transition-colors',
|
182 |
-
isCopied
|
183 |
-
? 'i-ph:check text-green-500'
|
184 |
-
: 'i-ph:copy text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary',
|
185 |
-
)}
|
186 |
-
/>
|
187 |
</button>
|
188 |
-
|
189 |
-
<button
|
190 |
-
onClick={() => setIsExpanded(!isExpanded)}
|
191 |
-
className="p-1 text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary transition-colors rounded focus:outline-none"
|
192 |
-
title="Toggle details"
|
193 |
-
>
|
194 |
-
<div
|
195 |
-
className={classNames('text-base transition-transform', {
|
196 |
-
'i-ph:caret-down rotate-180': isExpanded,
|
197 |
-
'i-ph:caret-down': !isExpanded,
|
198 |
-
})}
|
199 |
-
/>
|
200 |
-
</button>
|
201 |
-
)}
|
202 |
-
</div>
|
203 |
</div>
|
204 |
-
|
205 |
-
<div className="
|
206 |
-
<div className="i-ph:clock text-bolt-elements-textTertiary" />
|
207 |
-
<span className="text-bolt-elements-textSecondary">{formattedTime.primary}</span>
|
208 |
-
<span className="text-bolt-elements-textTertiary">·</span>
|
209 |
-
<span className="text-bolt-elements-textTertiary">{formattedTime.secondary}</span>
|
210 |
-
</div>
|
211 |
-
<span className="px-2 py-0.5 rounded-full bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3">
|
212 |
{log.category}
|
213 |
-
</
|
214 |
-
|
215 |
</div>
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
initial={{ height: 0, opacity: 0 }}
|
220 |
-
animate={{ height: 'auto', opacity: 1 }}
|
221 |
-
exit={{ height: 0, opacity: 0 }}
|
222 |
-
transition={{ duration: 0.2 }}
|
223 |
-
className="mt-2 px-3"
|
224 |
-
>
|
225 |
-
<pre className="p-2 text-sm rounded-md overflow-auto bg-bolt-elements-background-depth-2/50 dark:bg-bolt-elements-background-depth-3/50 text-bolt-elements-textSecondary">
|
226 |
{JSON.stringify(log.details, null, 2)}
|
227 |
</pre>
|
228 |
-
</
|
229 |
)}
|
230 |
</div>
|
231 |
);
|
232 |
};
|
233 |
|
234 |
-
/**
|
235 |
-
* TODO: Future Enhancements
|
236 |
-
*
|
237 |
-
* 1. Advanced Features:
|
238 |
-
* - Add export to JSON/CSV functionality
|
239 |
-
* - Implement log retention policies
|
240 |
-
* - Add custom alert rules and notifications
|
241 |
-
* - Add pattern detection and highlighting
|
242 |
-
*
|
243 |
-
* 2. Visual Improvements:
|
244 |
-
* - Add dark/light mode specific styling
|
245 |
-
* - Implement collapsible JSON viewer
|
246 |
-
* - Add timeline view with zoom capabilities
|
247 |
-
*
|
248 |
-
* 3. Performance Optimizations:
|
249 |
-
* - Implement virtualized scrolling for large logs
|
250 |
-
* - Add lazy loading for log details
|
251 |
-
* - Optimize search with indexing
|
252 |
-
*/
|
253 |
-
|
254 |
export function EventLogsTab() {
|
255 |
const logs = useStore(logStore.logs);
|
256 |
-
const [
|
257 |
-
const [
|
258 |
-
const [autoScroll, setAutoScroll] = useState(true);
|
259 |
const [searchQuery, setSearchQuery] = useState('');
|
260 |
-
const [
|
261 |
-
const [
|
|
|
|
|
|
|
262 |
const [isRefreshing, setIsRefreshing] = useState(false);
|
263 |
const logsContainerRef = useRef<HTMLDivElement>(null);
|
264 |
-
const
|
265 |
-
|
266 |
-
// Add refresh function
|
267 |
-
const handleRefresh = useCallback(async () => {
|
268 |
-
setIsRefreshing(true);
|
269 |
-
|
270 |
-
try {
|
271 |
-
// Since logStore doesn't have refresh, we'll re-fetch logs
|
272 |
-
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate refresh
|
273 |
-
toast.success('Logs refreshed');
|
274 |
-
} catch (err) {
|
275 |
-
console.error('Failed to refresh logs:', err);
|
276 |
-
toast.error('Failed to refresh logs');
|
277 |
-
} finally {
|
278 |
-
setIsRefreshing(false);
|
279 |
-
}
|
280 |
-
}, []);
|
281 |
|
282 |
const filteredLogs = useMemo(() => {
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
log.message?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
290 |
-
JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
|
291 |
-
|
292 |
-
return matchesLevel && matchesCategory && matchesSearch;
|
293 |
-
});
|
294 |
-
|
295 |
-
return filtered.reverse();
|
296 |
-
}, [logs, logLevel, logCategory, searchQuery]);
|
297 |
-
|
298 |
-
const handleClearLogs = useCallback(() => {
|
299 |
-
if (confirm('Are you sure you want to clear all logs?')) {
|
300 |
-
logStore.clearLogs();
|
301 |
-
toast.success('Logs cleared successfully');
|
302 |
-
}
|
303 |
-
}, []);
|
304 |
|
305 |
const handleExportLogs = useCallback(() => {
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
}, []);
|
332 |
|
333 |
-
|
334 |
-
|
|
|
|
|
|
|
|
|
335 |
|
336 |
-
|
337 |
-
|
338 |
-
|
|
|
339 |
|
340 |
-
|
341 |
-
const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
|
342 |
-
setIsScrolledToBottom(isBottom);
|
343 |
-
};
|
344 |
|
345 |
-
|
346 |
-
|
|
|
|
|
347 |
|
348 |
-
|
349 |
-
|
350 |
-
}
|
351 |
-
}, [filteredLogs, autoScroll, isScrolledToBottom]);
|
352 |
|
353 |
return (
|
354 |
<div className="flex flex-col h-full gap-4 p-6">
|
355 |
-
{/* Header
|
356 |
-
<div className="flex
|
357 |
-
|
358 |
-
|
359 |
-
<div
|
360 |
-
<
|
361 |
-
<
|
362 |
-
<h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Event Logs</h2>
|
363 |
-
<p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
|
364 |
-
</div>
|
365 |
</div>
|
|
|
|
|
366 |
<motion.button
|
367 |
onClick={handleRefresh}
|
368 |
-
|
369 |
-
className={classNames(
|
370 |
-
'p-2.5 rounded-lg',
|
371 |
-
'bg-purple-50/50 dark:bg-purple-900/10',
|
372 |
-
'text-purple-500 hover:text-purple-600',
|
373 |
-
'hover:bg-purple-100/50 dark:hover:bg-purple-900/20',
|
374 |
-
'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
|
375 |
-
'transition-all duration-200 ease-in-out',
|
376 |
-
{ 'opacity-50 cursor-not-allowed': isRefreshing },
|
377 |
-
)}
|
378 |
whileHover={{ scale: 1.05 }}
|
379 |
whileTap={{ scale: 0.95 }}
|
380 |
-
|
|
|
381 |
>
|
382 |
-
<div className=
|
383 |
-
<div className="i-ph:arrows-clockwise" />
|
384 |
-
</div>
|
385 |
</motion.button>
|
386 |
</div>
|
387 |
-
|
388 |
-
{/* Controls Section */}
|
389 |
-
<div className="flex items-center justify-end gap-2 px-1">
|
390 |
-
<div className="flex items-center gap-6 p-1.5 rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50">
|
391 |
-
<motion.div
|
392 |
-
className="flex items-center gap-3"
|
393 |
-
whileHover={{ scale: 1.02 }}
|
394 |
-
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
|
395 |
-
>
|
396 |
-
<span className="text-sm font-medium text-bolt-elements-textSecondary">Auto-scroll</span>
|
397 |
-
<Switch
|
398 |
-
checked={autoScroll}
|
399 |
-
onCheckedChange={setAutoScroll}
|
400 |
-
className="data-[state=checked]:bg-purple-500"
|
401 |
-
/>
|
402 |
-
</motion.div>
|
403 |
-
|
404 |
-
<div className="h-4 w-px bg-bolt-elements-borderColor" />
|
405 |
-
|
406 |
-
<motion.div
|
407 |
-
className="flex items-center gap-3"
|
408 |
-
whileHover={{ scale: 1.02 }}
|
409 |
-
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
|
410 |
-
>
|
411 |
-
<span className="text-sm font-medium text-bolt-elements-textSecondary">24h Time</span>
|
412 |
-
<Switch
|
413 |
-
checked={use24Hour}
|
414 |
-
onCheckedChange={setUse24Hour}
|
415 |
-
className="data-[state=checked]:bg-purple-500"
|
416 |
-
/>
|
417 |
-
</motion.div>
|
418 |
-
|
419 |
-
<div className="h-4 w-px bg-bolt-elements-borderColor" />
|
420 |
-
|
421 |
-
<motion.div
|
422 |
-
className="flex items-center gap-3"
|
423 |
-
whileHover={{ scale: 1.02 }}
|
424 |
-
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
|
425 |
-
>
|
426 |
-
<span className="text-sm font-medium text-bolt-elements-textSecondary">Expand All</span>
|
427 |
-
<Switch
|
428 |
-
checked={expandAll}
|
429 |
-
onCheckedChange={setExpandAll}
|
430 |
-
className="data-[state=checked]:bg-purple-500"
|
431 |
-
/>
|
432 |
-
</motion.div>
|
433 |
-
</div>
|
434 |
-
</div>
|
435 |
</div>
|
436 |
|
437 |
-
{/*
|
438 |
-
<div className="flex
|
439 |
-
|
440 |
-
|
441 |
-
<div className="i-ph:magnifying-glass text-base" />
|
442 |
-
</div>
|
443 |
<input
|
444 |
type="text"
|
445 |
placeholder="Search logs..."
|
446 |
-
className={classNames(
|
447 |
-
'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',
|
448 |
-
'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2',
|
449 |
-
'border border-bolt-elements-borderColor',
|
450 |
-
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
451 |
-
'focus:outline-none focus:ring-1 focus:ring-purple-500/30 focus:border-purple-500/30',
|
452 |
-
'transition-all duration-200',
|
453 |
-
)}
|
454 |
value={searchQuery}
|
455 |
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
456 |
/>
|
|
|
|
|
|
|
457 |
</div>
|
458 |
|
459 |
-
{/*
|
460 |
-
<div className="flex items-center -
|
461 |
-
<
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
/>
|
466 |
-
<div className="mx-2 w-px h-4 bg-bolt-elements-borderColor" />
|
467 |
-
<SegmentedGroup
|
468 |
-
value={logCategory}
|
469 |
-
onChange={(value) => setLogCategory(value as LogEntry['category'] | 'all')}
|
470 |
-
options={logCategoryOptions}
|
471 |
-
/>
|
472 |
-
</div>
|
473 |
-
</div>
|
474 |
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
))}
|
485 |
-
</div>
|
486 |
-
</div>
|
487 |
|
488 |
-
{/* Status Bar */}
|
489 |
-
<div className="flex items-center justify-between py-2 px-4 text-sm text-bolt-elements-textSecondary">
|
490 |
-
<div className="flex items-center gap-6">
|
491 |
-
<span>{filteredLogs.length} logs displayed</span>
|
492 |
-
<span>{isScrolledToBottom ? 'Watching for new logs...' : 'Scroll to bottom to watch new logs'}</span>
|
493 |
-
</div>
|
494 |
-
<div className="flex items-center gap-2">
|
495 |
<motion.button
|
496 |
onClick={handleExportLogs}
|
497 |
-
className="flex items-center gap-2 px-
|
498 |
whileHover={{ scale: 1.02 }}
|
499 |
whileTap={{ scale: 0.98 }}
|
500 |
>
|
501 |
-
<div className="i-ph:download-
|
502 |
-
Export
|
503 |
</motion.button>
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
509 |
>
|
510 |
-
<div
|
511 |
-
|
512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
513 |
</div>
|
514 |
</div>
|
515 |
</div>
|
|
|
1 |
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
import { Switch } from '~/components/ui/Switch';
|
4 |
import { logStore, type LogEntry } from '~/lib/stores/logs';
|
5 |
import { useStore } from '@nanostores/react';
|
6 |
import { classNames } from '~/utils/classNames';
|
|
|
7 |
|
8 |
interface SelectOption {
|
9 |
value: string;
|
10 |
label: string;
|
11 |
+
icon?: string;
|
12 |
color?: string;
|
13 |
}
|
14 |
|
15 |
const logLevelOptions: SelectOption[] = [
|
16 |
+
{
|
17 |
+
value: 'all',
|
18 |
+
label: 'All Levels',
|
19 |
+
icon: 'i-ph:funnel',
|
20 |
+
color: '#9333ea',
|
21 |
+
},
|
22 |
+
{
|
23 |
+
value: 'info',
|
24 |
+
label: 'Info',
|
25 |
+
icon: 'i-ph:info',
|
26 |
+
color: '#3b82f6',
|
27 |
+
},
|
28 |
+
{
|
29 |
+
value: 'warning',
|
30 |
+
label: 'Warning',
|
31 |
+
icon: 'i-ph:warning',
|
32 |
+
color: '#f59e0b',
|
33 |
+
},
|
34 |
+
{
|
35 |
+
value: 'error',
|
36 |
+
label: 'Error',
|
37 |
+
icon: 'i-ph:x-circle',
|
38 |
+
color: '#ef4444',
|
39 |
+
},
|
40 |
+
{
|
41 |
+
value: 'debug',
|
42 |
+
label: 'Debug',
|
43 |
+
icon: 'i-ph:bug',
|
44 |
+
color: '#6b7280',
|
45 |
+
},
|
46 |
];
|
47 |
|
48 |
const logCategoryOptions: SelectOption[] = [
|
49 |
+
{
|
50 |
+
value: 'all',
|
51 |
+
label: 'All Categories',
|
52 |
+
icon: 'i-ph:squares-four',
|
53 |
+
color: '#9333ea',
|
54 |
+
},
|
55 |
+
{
|
56 |
+
value: 'api',
|
57 |
+
label: 'API',
|
58 |
+
icon: 'i-ph:cloud',
|
59 |
+
color: '#3b82f6',
|
60 |
+
},
|
61 |
+
{
|
62 |
+
value: 'auth',
|
63 |
+
label: 'Auth',
|
64 |
+
icon: 'i-ph:key',
|
65 |
+
color: '#f59e0b',
|
66 |
+
},
|
67 |
+
{
|
68 |
+
value: 'database',
|
69 |
+
label: 'Database',
|
70 |
+
icon: 'i-ph:database',
|
71 |
+
color: '#10b981',
|
72 |
+
},
|
73 |
+
{
|
74 |
+
value: 'network',
|
75 |
+
label: 'Network',
|
76 |
+
icon: 'i-ph:wifi-high',
|
77 |
+
color: '#6366f1',
|
78 |
+
},
|
79 |
+
{
|
80 |
+
value: 'performance',
|
81 |
+
label: 'Performance',
|
82 |
+
icon: 'i-ph:chart-line-up',
|
83 |
+
color: '#8b5cf6',
|
84 |
+
},
|
85 |
];
|
86 |
|
87 |
+
interface LogEntryItemProps {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
log: LogEntry;
|
89 |
isExpanded: boolean;
|
90 |
use24Hour: boolean;
|
91 |
+
showTimestamp: boolean;
|
92 |
+
}
|
|
|
93 |
|
94 |
+
const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp }: LogEntryItemProps) => {
|
95 |
+
const [localExpanded, setLocalExpanded] = useState(forceExpanded);
|
96 |
+
|
97 |
+
// Update local expanded state when forceExpanded changes
|
98 |
useEffect(() => {
|
99 |
+
setLocalExpanded(forceExpanded);
|
100 |
}, [forceExpanded]);
|
101 |
|
102 |
+
const timestamp = useMemo(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
const date = new Date(log.timestamp);
|
104 |
+
|
105 |
+
if (use24Hour) {
|
106 |
+
return date.toLocaleTimeString('en-US', { hour12: false });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
}
|
108 |
+
|
109 |
+
return date.toLocaleTimeString('en-US', { hour12: true });
|
110 |
}, [log.timestamp, use24Hour]);
|
111 |
|
112 |
+
const levelColor = useMemo(() => {
|
113 |
+
switch (log.level) {
|
114 |
+
case 'error':
|
115 |
+
return 'text-red-500 bg-red-50 dark:bg-red-500/10';
|
116 |
+
case 'warning':
|
117 |
+
return 'text-yellow-500 bg-yellow-50 dark:bg-yellow-500/10';
|
118 |
+
case 'debug':
|
119 |
+
return 'text-gray-500 bg-gray-50 dark:bg-gray-500/10';
|
120 |
+
default:
|
121 |
+
return 'text-blue-500 bg-blue-50 dark:bg-blue-500/10';
|
122 |
+
}
|
123 |
+
}, [log.level]);
|
124 |
+
|
125 |
return (
|
126 |
+
<div className="p-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
127 |
+
<div className="flex items-start gap-3">
|
128 |
+
<div className={classNames('px-2 py-0.5 rounded text-xs font-medium uppercase', levelColor)}>{log.level}</div>
|
129 |
+
{showTimestamp && <div className="text-sm text-bolt-elements-textTertiary">{timestamp}</div>}
|
130 |
+
<div className="flex-grow">
|
131 |
+
<div className="text-sm text-bolt-elements-textPrimary">{log.message}</div>
|
132 |
+
{log.details && (
|
133 |
+
<button
|
134 |
+
onClick={() => setLocalExpanded(!localExpanded)}
|
135 |
+
className="mt-1 text-xs text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary"
|
136 |
+
>
|
137 |
+
{localExpanded ? 'Hide' : 'Show'} Details
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
</button>
|
139 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
</div>
|
141 |
+
{log.category && (
|
142 |
+
<div className="px-2 py-0.5 rounded-full text-xs bg-gray-100 dark:bg-gray-800 text-bolt-elements-textSecondary">
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
{log.category}
|
144 |
+
</div>
|
145 |
+
)}
|
146 |
</div>
|
147 |
+
{localExpanded && log.details && (
|
148 |
+
<div className="mt-2 p-2 rounded bg-gray-50 dark:bg-gray-800/50">
|
149 |
+
<pre className="text-xs text-bolt-elements-textSecondary whitespace-pre-wrap">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
{JSON.stringify(log.details, null, 2)}
|
151 |
</pre>
|
152 |
+
</div>
|
153 |
)}
|
154 |
</div>
|
155 |
);
|
156 |
};
|
157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
export function EventLogsTab() {
|
159 |
const logs = useStore(logStore.logs);
|
160 |
+
const [selectedLevel, setSelectedLevel] = useState('all');
|
161 |
+
const [selectedCategory, setSelectedCategory] = useState('all');
|
|
|
162 |
const [searchQuery, setSearchQuery] = useState('');
|
163 |
+
const [use24Hour, setUse24Hour] = useState(false);
|
164 |
+
const [autoExpand, setAutoExpand] = useState(false);
|
165 |
+
const [showTimestamps, setShowTimestamps] = useState(true);
|
166 |
+
const [showLevelFilter, setShowLevelFilter] = useState(false);
|
167 |
+
const [showCategoryFilter, setShowCategoryFilter] = useState(false);
|
168 |
const [isRefreshing, setIsRefreshing] = useState(false);
|
169 |
const logsContainerRef = useRef<HTMLDivElement>(null);
|
170 |
+
const levelFilterRef = useRef<HTMLDivElement>(null);
|
171 |
+
const categoryFilterRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
|
173 |
const filteredLogs = useMemo(() => {
|
174 |
+
return logStore.getFilteredLogs(
|
175 |
+
selectedLevel === 'all' ? undefined : (selectedLevel as LogEntry['level']),
|
176 |
+
selectedCategory === 'all' ? undefined : (selectedCategory as LogEntry['category']),
|
177 |
+
searchQuery,
|
178 |
+
);
|
179 |
+
}, [logs, selectedLevel, selectedCategory, searchQuery]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
|
181 |
const handleExportLogs = useCallback(() => {
|
182 |
+
const exportData = {
|
183 |
+
timestamp: new Date().toISOString(),
|
184 |
+
logs: filteredLogs,
|
185 |
+
filters: {
|
186 |
+
level: selectedLevel,
|
187 |
+
category: selectedCategory,
|
188 |
+
searchQuery,
|
189 |
+
},
|
190 |
+
};
|
191 |
+
|
192 |
+
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
193 |
+
const url = URL.createObjectURL(blob);
|
194 |
+
const a = document.createElement('a');
|
195 |
+
a.href = url;
|
196 |
+
a.download = `bolt-logs-${new Date().toISOString()}.json`;
|
197 |
+
document.body.appendChild(a);
|
198 |
+
a.click();
|
199 |
+
document.body.removeChild(a);
|
200 |
+
URL.revokeObjectURL(url);
|
201 |
+
}, [filteredLogs, selectedLevel, selectedCategory, searchQuery]);
|
202 |
+
|
203 |
+
const handleRefresh = useCallback(async () => {
|
204 |
+
setIsRefreshing(true);
|
205 |
+
await logStore.refreshLogs();
|
206 |
+
setTimeout(() => setIsRefreshing(false), 500); // Keep animation visible for at least 500ms
|
207 |
}, []);
|
208 |
|
209 |
+
// Close filters when clicking outside
|
210 |
+
useEffect(() => {
|
211 |
+
const handleClickOutside = (event: MouseEvent) => {
|
212 |
+
if (levelFilterRef.current && !levelFilterRef.current.contains(event.target as Node)) {
|
213 |
+
setShowLevelFilter(false);
|
214 |
+
}
|
215 |
|
216 |
+
if (categoryFilterRef.current && !categoryFilterRef.current.contains(event.target as Node)) {
|
217 |
+
setShowCategoryFilter(false);
|
218 |
+
}
|
219 |
+
};
|
220 |
|
221 |
+
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
|
|
|
222 |
|
223 |
+
return () => {
|
224 |
+
document.removeEventListener('mousedown', handleClickOutside);
|
225 |
+
};
|
226 |
+
}, []);
|
227 |
|
228 |
+
const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
|
229 |
+
const selectedCategoryOption = logCategoryOptions.find((opt) => opt.value === selectedCategory);
|
|
|
|
|
230 |
|
231 |
return (
|
232 |
<div className="flex flex-col h-full gap-4 p-6">
|
233 |
+
{/* Header */}
|
234 |
+
<div className="flex items-center justify-between">
|
235 |
+
<div className="flex items-center gap-3">
|
236 |
+
<div className="i-ph:list text-xl text-purple-500" />
|
237 |
+
<div>
|
238 |
+
<h2 className="text-lg font-semibold">Event Logs</h2>
|
239 |
+
<p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
|
|
|
|
|
|
|
240 |
</div>
|
241 |
+
</div>
|
242 |
+
<div className="flex items-center gap-4">
|
243 |
<motion.button
|
244 |
onClick={handleRefresh}
|
245 |
+
className="p-2 rounded-lg text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all duration-200"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
246 |
whileHover={{ scale: 1.05 }}
|
247 |
whileTap={{ scale: 0.95 }}
|
248 |
+
animate={isRefreshing ? { rotate: 360 } : {}}
|
249 |
+
transition={isRefreshing ? { duration: 1, repeat: Infinity, ease: 'linear' } : {}}
|
250 |
>
|
251 |
+
<div className="i-ph:arrows-clockwise text-xl" />
|
|
|
|
|
252 |
</motion.button>
|
253 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
254 |
</div>
|
255 |
|
256 |
+
{/* Top Controls */}
|
257 |
+
<div className="flex items-center gap-4">
|
258 |
+
{/* Search */}
|
259 |
+
<div className="flex-grow relative">
|
|
|
|
|
260 |
<input
|
261 |
type="text"
|
262 |
placeholder="Search logs..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
263 |
value={searchQuery}
|
264 |
onChange={(e) => setSearchQuery(e.target.value)}
|
265 |
+
className="w-full px-3 py-1.5 pl-9 rounded-lg text-sm bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50"
|
266 |
/>
|
267 |
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary">
|
268 |
+
<div className="i-ph:magnifying-glass text-base" />
|
269 |
+
</div>
|
270 |
</div>
|
271 |
|
272 |
+
{/* Right Controls */}
|
273 |
+
<div className="flex items-center gap-4">
|
274 |
+
<div className="flex items-center gap-2">
|
275 |
+
<Switch checked={showTimestamps} onCheckedChange={setShowTimestamps} />
|
276 |
+
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Timestamps</span>
|
277 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
278 |
|
279 |
+
<div className="flex items-center gap-2">
|
280 |
+
<Switch checked={use24Hour} onCheckedChange={setUse24Hour} />
|
281 |
+
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">24h Time</span>
|
282 |
+
</div>
|
283 |
+
|
284 |
+
<div className="flex items-center gap-2">
|
285 |
+
<Switch checked={autoExpand} onCheckedChange={setAutoExpand} />
|
286 |
+
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto Expand</span>
|
287 |
+
</div>
|
|
|
|
|
|
|
288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
<motion.button
|
290 |
onClick={handleExportLogs}
|
291 |
+
className="flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm bg-purple-500 hover:bg-purple-600 text-white transition-colors"
|
292 |
whileHover={{ scale: 1.02 }}
|
293 |
whileTap={{ scale: 0.98 }}
|
294 |
>
|
295 |
+
<div className="i-ph:download text-base" />
|
296 |
+
Export Logs
|
297 |
</motion.button>
|
298 |
+
</div>
|
299 |
+
</div>
|
300 |
+
|
301 |
+
{/* Filters */}
|
302 |
+
<div className="flex items-center gap-4 -mt-2">
|
303 |
+
{/* Level Filter */}
|
304 |
+
<div ref={levelFilterRef} className="relative">
|
305 |
+
<button
|
306 |
+
onClick={() => {
|
307 |
+
setShowLevelFilter(!showLevelFilter);
|
308 |
+
setShowCategoryFilter(false);
|
309 |
+
}}
|
310 |
+
className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
311 |
>
|
312 |
+
<div
|
313 |
+
className={classNames('text-sm', selectedLevelOption?.icon || 'i-ph:funnel')}
|
314 |
+
style={{ color: selectedLevelOption?.color }}
|
315 |
+
/>
|
316 |
+
<div className="text-sm text-bolt-elements-textSecondary">{selectedLevelOption?.label || 'All Levels'}</div>
|
317 |
+
<div
|
318 |
+
className={classNames(
|
319 |
+
'i-ph:caret-down text-sm text-bolt-elements-textTertiary transition-transform',
|
320 |
+
showLevelFilter ? 'rotate-180' : '',
|
321 |
+
)}
|
322 |
+
/>
|
323 |
+
</button>
|
324 |
+
|
325 |
+
{showLevelFilter && (
|
326 |
+
<div className="absolute left-0 top-full mt-1 z-20">
|
327 |
+
<div className="p-1 rounded-lg shadow-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
328 |
+
{logLevelOptions.map((option) => (
|
329 |
+
<button
|
330 |
+
key={option.value}
|
331 |
+
onClick={() => {
|
332 |
+
setSelectedLevel(option.value);
|
333 |
+
setShowLevelFilter(false);
|
334 |
+
}}
|
335 |
+
className={classNames(
|
336 |
+
'flex items-center gap-2 w-full px-3 py-1.5 rounded-md text-sm transition-colors',
|
337 |
+
option.value === selectedLevel
|
338 |
+
? 'bg-purple-100 dark:bg-purple-800/40 text-purple-900 dark:text-purple-200'
|
339 |
+
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50',
|
340 |
+
)}
|
341 |
+
>
|
342 |
+
<div className={classNames(option.icon, 'text-base', option.color)} />
|
343 |
+
<span>{option.label}</span>
|
344 |
+
</button>
|
345 |
+
))}
|
346 |
+
</div>
|
347 |
+
</div>
|
348 |
+
)}
|
349 |
+
</div>
|
350 |
+
|
351 |
+
<div className="w-px h-4 bg-gray-200 dark:bg-gray-700" />
|
352 |
+
|
353 |
+
{/* Category Filter */}
|
354 |
+
<div ref={categoryFilterRef} className="relative">
|
355 |
+
<button
|
356 |
+
onClick={() => {
|
357 |
+
setShowCategoryFilter(!showCategoryFilter);
|
358 |
+
setShowLevelFilter(false);
|
359 |
+
}}
|
360 |
+
className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
361 |
+
>
|
362 |
+
<div
|
363 |
+
className={classNames('text-sm', selectedCategoryOption?.icon || 'i-ph:squares-four')}
|
364 |
+
style={{ color: selectedCategoryOption?.color }}
|
365 |
+
/>
|
366 |
+
<div className="text-sm text-bolt-elements-textSecondary">
|
367 |
+
{selectedCategoryOption?.label || 'All Categories'}
|
368 |
+
</div>
|
369 |
+
<div
|
370 |
+
className={classNames(
|
371 |
+
'i-ph:caret-down text-sm text-bolt-elements-textTertiary transition-transform',
|
372 |
+
showCategoryFilter ? 'rotate-180' : '',
|
373 |
+
)}
|
374 |
+
/>
|
375 |
+
</button>
|
376 |
+
|
377 |
+
{showCategoryFilter && (
|
378 |
+
<div className="absolute left-0 top-full mt-1 z-20">
|
379 |
+
<div className="p-1 rounded-lg shadow-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
380 |
+
{logCategoryOptions.map((option) => (
|
381 |
+
<button
|
382 |
+
key={option.value}
|
383 |
+
onClick={() => {
|
384 |
+
setSelectedCategory(option.value);
|
385 |
+
setShowCategoryFilter(false);
|
386 |
+
}}
|
387 |
+
className={classNames(
|
388 |
+
'flex items-center gap-2 w-full px-3 py-1.5 rounded-md text-sm transition-colors',
|
389 |
+
option.value === selectedCategory
|
390 |
+
? 'bg-purple-100 dark:bg-purple-800/40 text-purple-900 dark:text-purple-200'
|
391 |
+
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50',
|
392 |
+
)}
|
393 |
+
>
|
394 |
+
<div className={classNames(option.icon, 'text-base', option.color)} />
|
395 |
+
<span>{option.label}</span>
|
396 |
+
</button>
|
397 |
+
))}
|
398 |
+
</div>
|
399 |
+
</div>
|
400 |
+
)}
|
401 |
+
</div>
|
402 |
+
</div>
|
403 |
+
|
404 |
+
{/* Logs Container */}
|
405 |
+
<div
|
406 |
+
ref={logsContainerRef}
|
407 |
+
className="flex-grow overflow-y-auto rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50"
|
408 |
+
>
|
409 |
+
<div className="divide-y divide-gray-200/50 dark:divide-gray-700/50">
|
410 |
+
{filteredLogs.map((log) => (
|
411 |
+
<LogEntryItem
|
412 |
+
key={log.id}
|
413 |
+
log={log}
|
414 |
+
isExpanded={autoExpand}
|
415 |
+
use24Hour={use24Hour}
|
416 |
+
showTimestamp={showTimestamps}
|
417 |
+
/>
|
418 |
+
))}
|
419 |
</div>
|
420 |
</div>
|
421 |
</div>
|
app/components/settings/task-manager/TaskManagerTab.tsx
CHANGED
@@ -11,6 +11,7 @@ import {
|
|
11 |
Tooltip,
|
12 |
Legend,
|
13 |
} from 'chart.js';
|
|
|
14 |
|
15 |
// Register ChartJS components
|
16 |
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
@@ -120,8 +121,18 @@ export default function TaskManagerTab() {
|
|
120 |
metrics: false,
|
121 |
processes: false,
|
122 |
});
|
123 |
-
const [energySaverMode, setEnergySaverMode] = useState(
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
const [energySavings, setEnergySavings] = useState<EnergySavings>({
|
126 |
updatesReduced: 0,
|
127 |
timeInSaverMode: 0,
|
@@ -130,6 +141,26 @@ export default function TaskManagerTab() {
|
|
130 |
|
131 |
const saverModeStartTime = useRef<number | null>(null);
|
132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
// Calculate energy savings
|
134 |
const updateEnergySavings = useCallback(() => {
|
135 |
if (!energySaverMode) {
|
@@ -163,17 +194,25 @@ export default function TaskManagerTab() {
|
|
163 |
}, [energySaverMode]);
|
164 |
|
165 |
useEffect((): (() => void) | undefined => {
|
166 |
-
if (energySaverMode) {
|
167 |
-
|
168 |
-
|
|
|
|
|
|
|
|
|
|
|
169 |
}
|
170 |
|
171 |
-
|
|
|
|
|
172 |
}, [energySaverMode, updateEnergySavings]);
|
173 |
|
174 |
-
// Auto energy saver effect
|
175 |
useEffect((): (() => void) | undefined => {
|
176 |
if (!autoEnergySaver) {
|
|
|
|
|
177 |
return undefined;
|
178 |
}
|
179 |
|
@@ -194,18 +233,6 @@ export default function TaskManagerTab() {
|
|
194 |
return () => clearInterval(batteryCheckInterval);
|
195 |
}, [autoEnergySaver]);
|
196 |
|
197 |
-
const getStatusColor = (status: 'active' | 'idle' | 'suspended'): string => {
|
198 |
-
if (status === 'active') {
|
199 |
-
return 'text-green-500';
|
200 |
-
}
|
201 |
-
|
202 |
-
if (status === 'suspended') {
|
203 |
-
return 'text-yellow-500';
|
204 |
-
}
|
205 |
-
|
206 |
-
return 'text-gray-400';
|
207 |
-
};
|
208 |
-
|
209 |
const getUsageColor = (usage: number): string => {
|
210 |
if (usage > 80) {
|
211 |
return 'text-red-500';
|
@@ -215,7 +242,7 @@ export default function TaskManagerTab() {
|
|
215 |
return 'text-yellow-500';
|
216 |
}
|
217 |
|
218 |
-
return 'text-
|
219 |
};
|
220 |
|
221 |
const getImpactColor = (impact: 'high' | 'medium' | 'low'): string => {
|
@@ -227,7 +254,7 @@ export default function TaskManagerTab() {
|
|
227 |
return 'text-yellow-500';
|
228 |
}
|
229 |
|
230 |
-
return 'text-
|
231 |
};
|
232 |
|
233 |
const renderUsageGraph = (data: number[], label: string, color: string) => {
|
@@ -359,7 +386,7 @@ export default function TaskManagerTab() {
|
|
359 |
type: 'Network',
|
360 |
cpuUsage: Math.random() * 5,
|
361 |
memoryUsage: Math.random() * 50,
|
362 |
-
status: '
|
363 |
lastUpdate: new Date().toISOString(),
|
364 |
impact: 'high',
|
365 |
},
|
@@ -368,7 +395,7 @@ export default function TaskManagerTab() {
|
|
368 |
type: 'Animation',
|
369 |
cpuUsage: Math.random() * 3,
|
370 |
memoryUsage: Math.random() * 30,
|
371 |
-
status: '
|
372 |
lastUpdate: new Date().toISOString(),
|
373 |
impact: 'medium',
|
374 |
},
|
@@ -386,7 +413,7 @@ export default function TaskManagerTab() {
|
|
386 |
type: 'Storage',
|
387 |
cpuUsage: Math.random() * 1,
|
388 |
memoryUsage: Math.random() * 15,
|
389 |
-
status: '
|
390 |
lastUpdate: new Date().toISOString(),
|
391 |
impact: 'low',
|
392 |
},
|
@@ -395,7 +422,7 @@ export default function TaskManagerTab() {
|
|
395 |
type: 'Network',
|
396 |
cpuUsage: Math.random() * 2,
|
397 |
memoryUsage: Math.random() * 10,
|
398 |
-
status: '
|
399 |
lastUpdate: new Date().toISOString(),
|
400 |
impact: 'medium',
|
401 |
},
|
@@ -444,7 +471,7 @@ export default function TaskManagerTab() {
|
|
444 |
type="checkbox"
|
445 |
id="autoEnergySaver"
|
446 |
checked={autoEnergySaver}
|
447 |
-
onChange={(e) =>
|
448 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
|
449 |
/>
|
450 |
<label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
|
@@ -456,7 +483,7 @@ export default function TaskManagerTab() {
|
|
456 |
type="checkbox"
|
457 |
id="energySaver"
|
458 |
checked={energySaverMode}
|
459 |
-
onChange={(e) => !autoEnergySaver &&
|
460 |
disabled={autoEnergySaver}
|
461 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
|
462 |
/>
|
@@ -465,7 +492,7 @@ export default function TaskManagerTab() {
|
|
465 |
className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
|
466 |
>
|
467 |
Energy Saver
|
468 |
-
{energySaverMode && <span className="ml-2 text-xs text-
|
469 |
</label>
|
470 |
</div>
|
471 |
</div>
|
@@ -502,7 +529,7 @@ export default function TaskManagerTab() {
|
|
502 |
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
503 |
{Math.round(metrics.battery.level)}%
|
504 |
{metrics.battery.charging && (
|
505 |
-
<span className="ml-2 text-
|
506 |
<div className="i-ph:lightning-fill w-4 h-4 inline-block" />
|
507 |
</span>
|
508 |
)}
|
@@ -597,10 +624,9 @@ export default function TaskManagerTab() {
|
|
597 |
</span>
|
598 |
</td>
|
599 |
<td className="py-3 px-4">
|
600 |
-
<
|
601 |
-
|
602 |
-
|
603 |
-
</div>
|
604 |
</td>
|
605 |
<td className="py-3 px-4">
|
606 |
<span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
|
|
|
11 |
Tooltip,
|
12 |
Legend,
|
13 |
} from 'chart.js';
|
14 |
+
import { toast } from 'react-toastify'; // Import toast
|
15 |
|
16 |
// Register ChartJS components
|
17 |
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
|
|
121 |
metrics: false,
|
122 |
processes: false,
|
123 |
});
|
124 |
+
const [energySaverMode, setEnergySaverMode] = useState<boolean>(() => {
|
125 |
+
// Initialize from localStorage, default to false
|
126 |
+
const saved = localStorage.getItem('energySaverMode');
|
127 |
+
return saved ? JSON.parse(saved) : false;
|
128 |
+
});
|
129 |
+
|
130 |
+
const [autoEnergySaver, setAutoEnergySaver] = useState<boolean>(() => {
|
131 |
+
// Initialize from localStorage, default to false
|
132 |
+
const saved = localStorage.getItem('autoEnergySaver');
|
133 |
+
return saved ? JSON.parse(saved) : false;
|
134 |
+
});
|
135 |
+
|
136 |
const [energySavings, setEnergySavings] = useState<EnergySavings>({
|
137 |
updatesReduced: 0,
|
138 |
timeInSaverMode: 0,
|
|
|
141 |
|
142 |
const saverModeStartTime = useRef<number | null>(null);
|
143 |
|
144 |
+
// Handle energy saver mode changes
|
145 |
+
const handleEnergySaverChange = (checked: boolean) => {
|
146 |
+
setEnergySaverMode(checked);
|
147 |
+
localStorage.setItem('energySaverMode', JSON.stringify(checked));
|
148 |
+
toast.success(checked ? 'Energy Saver mode enabled' : 'Energy Saver mode disabled');
|
149 |
+
};
|
150 |
+
|
151 |
+
// Handle auto energy saver changes
|
152 |
+
const handleAutoEnergySaverChange = (checked: boolean) => {
|
153 |
+
setAutoEnergySaver(checked);
|
154 |
+
localStorage.setItem('autoEnergySaver', JSON.stringify(checked));
|
155 |
+
toast.success(checked ? 'Auto Energy Saver enabled' : 'Auto Energy Saver disabled');
|
156 |
+
|
157 |
+
if (!checked) {
|
158 |
+
// When disabling auto mode, also disable energy saver mode
|
159 |
+
setEnergySaverMode(false);
|
160 |
+
localStorage.setItem('energySaverMode', 'false');
|
161 |
+
}
|
162 |
+
};
|
163 |
+
|
164 |
// Calculate energy savings
|
165 |
const updateEnergySavings = useCallback(() => {
|
166 |
if (!energySaverMode) {
|
|
|
194 |
}, [energySaverMode]);
|
195 |
|
196 |
useEffect((): (() => void) | undefined => {
|
197 |
+
if (!energySaverMode) {
|
198 |
+
// Clear any existing intervals and reset savings when disabled
|
199 |
+
setEnergySavings({
|
200 |
+
updatesReduced: 0,
|
201 |
+
timeInSaverMode: 0,
|
202 |
+
estimatedEnergySaved: 0,
|
203 |
+
});
|
204 |
+
return undefined;
|
205 |
}
|
206 |
|
207 |
+
const savingsInterval = setInterval(updateEnergySavings, 1000);
|
208 |
+
|
209 |
+
return () => clearInterval(savingsInterval);
|
210 |
}, [energySaverMode, updateEnergySavings]);
|
211 |
|
|
|
212 |
useEffect((): (() => void) | undefined => {
|
213 |
if (!autoEnergySaver) {
|
214 |
+
// If auto mode is disabled, clear any forced energy saver state
|
215 |
+
setEnergySaverMode(false);
|
216 |
return undefined;
|
217 |
}
|
218 |
|
|
|
233 |
return () => clearInterval(batteryCheckInterval);
|
234 |
}, [autoEnergySaver]);
|
235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
const getUsageColor = (usage: number): string => {
|
237 |
if (usage > 80) {
|
238 |
return 'text-red-500';
|
|
|
242 |
return 'text-yellow-500';
|
243 |
}
|
244 |
|
245 |
+
return 'text-gray-500';
|
246 |
};
|
247 |
|
248 |
const getImpactColor = (impact: 'high' | 'medium' | 'low'): string => {
|
|
|
254 |
return 'text-yellow-500';
|
255 |
}
|
256 |
|
257 |
+
return 'text-gray-500';
|
258 |
};
|
259 |
|
260 |
const renderUsageGraph = (data: number[], label: string, color: string) => {
|
|
|
386 |
type: 'Network',
|
387 |
cpuUsage: Math.random() * 5,
|
388 |
memoryUsage: Math.random() * 50,
|
389 |
+
status: 'idle',
|
390 |
lastUpdate: new Date().toISOString(),
|
391 |
impact: 'high',
|
392 |
},
|
|
|
395 |
type: 'Animation',
|
396 |
cpuUsage: Math.random() * 3,
|
397 |
memoryUsage: Math.random() * 30,
|
398 |
+
status: 'idle',
|
399 |
lastUpdate: new Date().toISOString(),
|
400 |
impact: 'medium',
|
401 |
},
|
|
|
413 |
type: 'Storage',
|
414 |
cpuUsage: Math.random() * 1,
|
415 |
memoryUsage: Math.random() * 15,
|
416 |
+
status: 'idle',
|
417 |
lastUpdate: new Date().toISOString(),
|
418 |
impact: 'low',
|
419 |
},
|
|
|
422 |
type: 'Network',
|
423 |
cpuUsage: Math.random() * 2,
|
424 |
memoryUsage: Math.random() * 10,
|
425 |
+
status: 'idle',
|
426 |
lastUpdate: new Date().toISOString(),
|
427 |
impact: 'medium',
|
428 |
},
|
|
|
471 |
type="checkbox"
|
472 |
id="autoEnergySaver"
|
473 |
checked={autoEnergySaver}
|
474 |
+
onChange={(e) => handleAutoEnergySaverChange(e.target.checked)}
|
475 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
|
476 |
/>
|
477 |
<label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
|
|
|
483 |
type="checkbox"
|
484 |
id="energySaver"
|
485 |
checked={energySaverMode}
|
486 |
+
onChange={(e) => !autoEnergySaver && handleEnergySaverChange(e.target.checked)}
|
487 |
disabled={autoEnergySaver}
|
488 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
|
489 |
/>
|
|
|
492 |
className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
|
493 |
>
|
494 |
Energy Saver
|
495 |
+
{energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>}
|
496 |
</label>
|
497 |
</div>
|
498 |
</div>
|
|
|
529 |
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
530 |
{Math.round(metrics.battery.level)}%
|
531 |
{metrics.battery.charging && (
|
532 |
+
<span className="ml-2 text-bolt-elements-textSecondary">
|
533 |
<div className="i-ph:lightning-fill w-4 h-4 inline-block" />
|
534 |
</span>
|
535 |
)}
|
|
|
624 |
</span>
|
625 |
</td>
|
626 |
<td className="py-3 px-4">
|
627 |
+
<span className={classNames('text-sm text-bolt-elements-textSecondary capitalize')}>
|
628 |
+
{process.status}
|
629 |
+
</span>
|
|
|
630 |
</td>
|
631 |
<td className="py-3 px-4">
|
632 |
<span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
|
app/components/settings/update/UpdateTab.tsx
CHANGED
@@ -707,7 +707,7 @@ const UpdateTab = () => {
|
|
707 |
<div className="max-h-[400px] overflow-y-auto">
|
708 |
{categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
|
709 |
<div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
|
710 |
-
<div className="p-3 bg-
|
711 |
<h5 className="text-sm font-medium text-bolt-elements-textPrimary">
|
712 |
{category}
|
713 |
<span className="ml-2 text-xs text-bolt-elements-textSecondary">
|
|
|
707 |
<div className="max-h-[400px] overflow-y-auto">
|
708 |
{categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
|
709 |
<div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
|
710 |
+
<div className="p-3 bg-[#EAEAEA] dark:bg-[#2A2A2A]">
|
711 |
<h5 className="text-sm font-medium text-bolt-elements-textPrimary">
|
712 |
{category}
|
713 |
<span className="ml-2 text-xs text-bolt-elements-textSecondary">
|
app/lib/stores/logs.ts
CHANGED
@@ -10,10 +10,12 @@ export interface LogEntry {
|
|
10 |
level: 'info' | 'warning' | 'error' | 'debug';
|
11 |
message: string;
|
12 |
details?: Record<string, any>;
|
13 |
-
category: 'system' | 'provider' | 'user' | 'error' | 'api' | 'auth' | 'database' | 'network';
|
14 |
subCategory?: string;
|
15 |
duration?: number;
|
16 |
statusCode?: number;
|
|
|
|
|
17 |
}
|
18 |
|
19 |
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
|
@@ -109,6 +111,8 @@ class LogStore {
|
|
109 |
level: LogEntry['level'] = 'info',
|
110 |
category: LogEntry['category'] = 'system',
|
111 |
details?: Record<string, any>,
|
|
|
|
|
112 |
) {
|
113 |
const id = this._generateId();
|
114 |
const entry: LogEntry = {
|
@@ -118,6 +122,8 @@ class LogStore {
|
|
118 |
message,
|
119 |
details,
|
120 |
category,
|
|
|
|
|
121 |
};
|
122 |
|
123 |
this._logs.setKey(id, entry);
|
@@ -262,6 +268,94 @@ class LogStore {
|
|
262 |
this._readLogs.clear();
|
263 |
this._saveReadLogs();
|
264 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
265 |
}
|
266 |
|
267 |
export const logStore = new LogStore();
|
|
|
10 |
level: 'info' | 'warning' | 'error' | 'debug';
|
11 |
message: string;
|
12 |
details?: Record<string, any>;
|
13 |
+
category: 'system' | 'provider' | 'user' | 'error' | 'api' | 'auth' | 'database' | 'network' | 'performance';
|
14 |
subCategory?: string;
|
15 |
duration?: number;
|
16 |
statusCode?: number;
|
17 |
+
source?: string;
|
18 |
+
stack?: string;
|
19 |
}
|
20 |
|
21 |
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
|
|
|
111 |
level: LogEntry['level'] = 'info',
|
112 |
category: LogEntry['category'] = 'system',
|
113 |
details?: Record<string, any>,
|
114 |
+
statusCode?: number,
|
115 |
+
duration?: number,
|
116 |
) {
|
117 |
const id = this._generateId();
|
118 |
const entry: LogEntry = {
|
|
|
122 |
message,
|
123 |
details,
|
124 |
category,
|
125 |
+
statusCode,
|
126 |
+
duration,
|
127 |
};
|
128 |
|
129 |
this._logs.setKey(id, entry);
|
|
|
268 |
this._readLogs.clear();
|
269 |
this._saveReadLogs();
|
270 |
}
|
271 |
+
|
272 |
+
// Network request logging
|
273 |
+
logNetworkRequest(
|
274 |
+
method: string,
|
275 |
+
url: string,
|
276 |
+
statusCode: number,
|
277 |
+
duration: number,
|
278 |
+
requestData?: any,
|
279 |
+
responseData?: any,
|
280 |
+
) {
|
281 |
+
this.addLog(
|
282 |
+
`${method} ${url}`,
|
283 |
+
statusCode >= 400 ? 'error' : 'info',
|
284 |
+
'network',
|
285 |
+
{
|
286 |
+
method,
|
287 |
+
url,
|
288 |
+
statusCode,
|
289 |
+
duration,
|
290 |
+
request: requestData,
|
291 |
+
response: responseData,
|
292 |
+
},
|
293 |
+
statusCode,
|
294 |
+
duration,
|
295 |
+
);
|
296 |
+
}
|
297 |
+
|
298 |
+
// Authentication events
|
299 |
+
logAuthEvent(event: string, success: boolean, details?: Record<string, any>) {
|
300 |
+
this.addLog(`Auth ${event} ${success ? 'succeeded' : 'failed'}`, success ? 'info' : 'error', 'auth', details);
|
301 |
+
}
|
302 |
+
|
303 |
+
// API interactions
|
304 |
+
logApiCall(
|
305 |
+
endpoint: string,
|
306 |
+
method: string,
|
307 |
+
statusCode: number,
|
308 |
+
duration: number,
|
309 |
+
requestData?: any,
|
310 |
+
responseData?: any,
|
311 |
+
) {
|
312 |
+
this.addLog(
|
313 |
+
`API ${method} ${endpoint}`,
|
314 |
+
statusCode >= 400 ? 'error' : 'info',
|
315 |
+
'api',
|
316 |
+
{
|
317 |
+
endpoint,
|
318 |
+
method,
|
319 |
+
statusCode,
|
320 |
+
duration,
|
321 |
+
request: requestData,
|
322 |
+
response: responseData,
|
323 |
+
},
|
324 |
+
statusCode,
|
325 |
+
duration,
|
326 |
+
);
|
327 |
+
}
|
328 |
+
|
329 |
+
// Performance monitoring
|
330 |
+
logPerformance(operation: string, duration: number, details?: Record<string, any>) {
|
331 |
+
this.addLog(
|
332 |
+
`Performance: ${operation}`,
|
333 |
+
duration > 1000 ? 'warning' : 'info',
|
334 |
+
'performance',
|
335 |
+
{
|
336 |
+
operation,
|
337 |
+
duration,
|
338 |
+
...details,
|
339 |
+
},
|
340 |
+
undefined,
|
341 |
+
duration,
|
342 |
+
);
|
343 |
+
}
|
344 |
+
|
345 |
+
// Error logging with stack trace
|
346 |
+
logErrorWithStack(error: Error, category: LogEntry['category'] = 'error', details?: Record<string, any>) {
|
347 |
+
this.addLog(error.message, 'error', category, {
|
348 |
+
...details,
|
349 |
+
name: error.name,
|
350 |
+
stack: error.stack,
|
351 |
+
});
|
352 |
+
}
|
353 |
+
|
354 |
+
// Refresh logs (useful for real-time updates)
|
355 |
+
refreshLogs() {
|
356 |
+
const currentLogs = this._logs.get();
|
357 |
+
this._logs.set({ ...currentLogs });
|
358 |
+
}
|
359 |
}
|
360 |
|
361 |
export const logStore = new LogStore();
|
app/routes/api.system.app-info.ts
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { ActionFunctionArgs } from '@remix-run/cloudflare';
|
2 |
+
import { json } from '@remix-run/cloudflare';
|
3 |
+
|
4 |
+
interface PackageJson {
|
5 |
+
name: string;
|
6 |
+
version: string;
|
7 |
+
description: string;
|
8 |
+
license: string;
|
9 |
+
dependencies: Record<string, string>;
|
10 |
+
devDependencies: Record<string, string>;
|
11 |
+
}
|
12 |
+
|
13 |
+
const packageJson = {
|
14 |
+
name: 'bolt.diy',
|
15 |
+
version: '0.1.0',
|
16 |
+
description: 'A DIY LLM interface',
|
17 |
+
license: 'MIT',
|
18 |
+
dependencies: {
|
19 |
+
'@remix-run/cloudflare': '^2.0.0',
|
20 |
+
react: '^18.0.0',
|
21 |
+
'react-dom': '^18.0.0',
|
22 |
+
typescript: '^5.0.0',
|
23 |
+
},
|
24 |
+
devDependencies: {
|
25 |
+
'@types/react': '^18.0.0',
|
26 |
+
'@types/react-dom': '^18.0.0',
|
27 |
+
},
|
28 |
+
} as PackageJson;
|
29 |
+
|
30 |
+
export const action = async ({ request: _request }: ActionFunctionArgs) => {
|
31 |
+
try {
|
32 |
+
return json({
|
33 |
+
name: packageJson.name,
|
34 |
+
version: packageJson.version,
|
35 |
+
description: packageJson.description,
|
36 |
+
license: packageJson.license,
|
37 |
+
nodeVersion: process.version,
|
38 |
+
dependencies: packageJson.dependencies,
|
39 |
+
devDependencies: packageJson.devDependencies,
|
40 |
+
});
|
41 |
+
} catch (error) {
|
42 |
+
console.error('Failed to get webapp info:', error);
|
43 |
+
return json({ error: 'Failed to get webapp information' }, { status: 500 });
|
44 |
+
}
|
45 |
+
};
|
changelogUI.md
DELETED
@@ -1,214 +0,0 @@
|
|
1 |
-
# Bolt DIY UI Overhaul
|
2 |
-
|
3 |
-
## New User Interface Features
|
4 |
-
|
5 |
-
### 🎨 Redesigned Control Panel
|
6 |
-
|
7 |
-
The Bolt DIY interface has been completely redesigned with a modern, intuitive layout featuring two main components:
|
8 |
-
|
9 |
-
1. **Users Window** - Main control panel for regular users
|
10 |
-
2. **Developer Window** - Advanced settings and debugging tools
|
11 |
-
|
12 |
-
### 💡 Core Features
|
13 |
-
|
14 |
-
- **Drag & Drop Tab Management**: Customize tab order in both User and Developer windows
|
15 |
-
- **Dynamic Status Updates**: Real-time status indicators for updates, notifications, and system health
|
16 |
-
- **Responsive Design**: Beautiful transitions and animations using Framer Motion
|
17 |
-
- **Dark/Light Mode Support**: Full theme support with consistent styling
|
18 |
-
- **Improved Accessibility**: Using Radix UI primitives for better accessibility
|
19 |
-
- **Enhanced Provider Management**: Split view for local and cloud providers
|
20 |
-
- **Resource Monitoring**: New Task Manager for system performance tracking
|
21 |
-
|
22 |
-
### 🎯 Tab Overview
|
23 |
-
|
24 |
-
#### User Window Tabs
|
25 |
-
|
26 |
-
1. **Profile**
|
27 |
-
|
28 |
-
- Manage user profile and account settings
|
29 |
-
- Avatar customization
|
30 |
-
- Account preferences
|
31 |
-
|
32 |
-
2. **Settings**
|
33 |
-
|
34 |
-
- Configure application preferences
|
35 |
-
- Customize UI behavior
|
36 |
-
- Manage general settings
|
37 |
-
|
38 |
-
3. **Notifications**
|
39 |
-
|
40 |
-
- Real-time notification center
|
41 |
-
- Unread notification tracking
|
42 |
-
- Notification preferences
|
43 |
-
|
44 |
-
4. **Features**
|
45 |
-
|
46 |
-
- Explore new and upcoming features
|
47 |
-
- Feature preview toggles
|
48 |
-
- Early access options
|
49 |
-
|
50 |
-
5. **Data**
|
51 |
-
|
52 |
-
- Data management tools
|
53 |
-
- Storage settings
|
54 |
-
- Backup and restore options
|
55 |
-
|
56 |
-
6. **Cloud Providers**
|
57 |
-
|
58 |
-
- Configure cloud-based AI providers
|
59 |
-
- API key management
|
60 |
-
- Cloud model selection
|
61 |
-
- Provider-specific settings
|
62 |
-
- Status monitoring for each provider
|
63 |
-
|
64 |
-
7. **Local Providers**
|
65 |
-
|
66 |
-
- Manage local AI models
|
67 |
-
- Ollama integration and model updates
|
68 |
-
- LM Studio configuration
|
69 |
-
- Local inference settings
|
70 |
-
- Model download and updates
|
71 |
-
|
72 |
-
8. **Task Manager**
|
73 |
-
|
74 |
-
- System resource monitoring
|
75 |
-
- Process management
|
76 |
-
- Performance metrics
|
77 |
-
- Resource usage graphs
|
78 |
-
- Alert configurations
|
79 |
-
|
80 |
-
9. **Connection**
|
81 |
-
|
82 |
-
- Network status monitoring
|
83 |
-
- Connection health metrics
|
84 |
-
- Troubleshooting tools
|
85 |
-
- Latency tracking
|
86 |
-
- Auto-reconnect settings
|
87 |
-
|
88 |
-
10. **Debug**
|
89 |
-
|
90 |
-
- System diagnostics
|
91 |
-
- Performance monitoring
|
92 |
-
- Error tracking
|
93 |
-
- Provider status checks
|
94 |
-
- System information
|
95 |
-
|
96 |
-
11. **Event Logs**
|
97 |
-
|
98 |
-
- Comprehensive system logs
|
99 |
-
- Filtered log views
|
100 |
-
- Log management tools
|
101 |
-
- Error tracking
|
102 |
-
- Performance metrics
|
103 |
-
|
104 |
-
12. **Update**
|
105 |
-
- Version management
|
106 |
-
- Update notifications
|
107 |
-
- Release notes
|
108 |
-
- Auto-update configuration
|
109 |
-
|
110 |
-
#### Developer Window Enhancements
|
111 |
-
|
112 |
-
- **Advanced Tab Management**
|
113 |
-
|
114 |
-
- Fine-grained control over tab visibility
|
115 |
-
- Custom tab ordering
|
116 |
-
- Tab permission management
|
117 |
-
- Category-based organization
|
118 |
-
|
119 |
-
- **Developer Tools**
|
120 |
-
- Enhanced debugging capabilities
|
121 |
-
- System metrics and monitoring
|
122 |
-
- Performance optimization tools
|
123 |
-
- Advanced logging features
|
124 |
-
|
125 |
-
### 🚀 UI Improvements
|
126 |
-
|
127 |
-
1. **Enhanced Navigation**
|
128 |
-
|
129 |
-
- Intuitive back navigation
|
130 |
-
- Breadcrumb-style header
|
131 |
-
- Context-aware menu system
|
132 |
-
- Improved tab organization
|
133 |
-
|
134 |
-
2. **Status Indicators**
|
135 |
-
|
136 |
-
- Dynamic update badges
|
137 |
-
- Real-time connection status
|
138 |
-
- System health monitoring
|
139 |
-
- Provider status tracking
|
140 |
-
|
141 |
-
3. **Profile Integration**
|
142 |
-
|
143 |
-
- Quick access profile menu
|
144 |
-
- Avatar support
|
145 |
-
- Fast settings access
|
146 |
-
- Personalization options
|
147 |
-
|
148 |
-
4. **Accessibility Features**
|
149 |
-
- Keyboard navigation
|
150 |
-
- Screen reader support
|
151 |
-
- Focus management
|
152 |
-
- ARIA attributes
|
153 |
-
|
154 |
-
### 🛠 Technical Enhancements
|
155 |
-
|
156 |
-
- **State Management**
|
157 |
-
|
158 |
-
- Nano Stores for efficient state handling
|
159 |
-
- Persistent settings storage
|
160 |
-
- Real-time state synchronization
|
161 |
-
- Provider state management
|
162 |
-
|
163 |
-
- **Performance Optimizations**
|
164 |
-
|
165 |
-
- Lazy loading of tab contents
|
166 |
-
- Efficient DOM updates
|
167 |
-
- Optimized animations
|
168 |
-
- Resource monitoring
|
169 |
-
|
170 |
-
- **Developer Experience**
|
171 |
-
- Improved error handling
|
172 |
-
- Better debugging tools
|
173 |
-
- Enhanced logging system
|
174 |
-
- Performance profiling
|
175 |
-
|
176 |
-
### 🎯 Future Roadmap
|
177 |
-
|
178 |
-
- [ ] Additional customization options
|
179 |
-
- [ ] Enhanced theme support
|
180 |
-
- [ ] More developer tools
|
181 |
-
- [ ] Extended API integrations
|
182 |
-
- [ ] Advanced monitoring capabilities
|
183 |
-
- [ ] Custom provider plugins
|
184 |
-
- [ ] Enhanced resource management
|
185 |
-
- [ ] Advanced debugging features
|
186 |
-
|
187 |
-
## 🔧 Technical Details
|
188 |
-
|
189 |
-
### Dependencies
|
190 |
-
|
191 |
-
- Radix UI for accessible components
|
192 |
-
- Framer Motion for animations
|
193 |
-
- React DnD for drag and drop
|
194 |
-
- Nano Stores for state management
|
195 |
-
|
196 |
-
### Browser Support
|
197 |
-
|
198 |
-
- Modern browsers (Chrome, Firefox, Safari, Edge)
|
199 |
-
- Progressive enhancement for older browsers
|
200 |
-
|
201 |
-
### Performance
|
202 |
-
|
203 |
-
- Optimized bundle size
|
204 |
-
- Efficient state updates
|
205 |
-
- Minimal re-renders
|
206 |
-
- Resource-aware operations
|
207 |
-
|
208 |
-
## 📝 Contributing
|
209 |
-
|
210 |
-
We welcome contributions! Please see our contributing guidelines for more information.
|
211 |
-
|
212 |
-
## 📄 License
|
213 |
-
|
214 |
-
MIT License - see LICENSE for details
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|