feat: add log level control to user settings

- Add logLevel field to UserProfile interface
- Update logger to support 'none' level and remove production restrictions
- Add log level selector in settings UI with immediate effect
- Remove unused preview synchronization code
- Update @webcontainer/api dependency version
This commit is contained in:
KevIsDev
2025-06-20 13:57:05 +01:00
parent c9703b132a
commit 75acdc9afb
8 changed files with 82 additions and 239 deletions

View File

@@ -25,6 +25,7 @@ export interface UserProfile {
bio?: string;
language: string;
timezone: string;
logLevel?: 'none' | 'trace' | 'debug' | 'info' | 'warn' | 'error';
}
export interface SettingItem {

View File

@@ -5,6 +5,7 @@ import { classNames } from '~/shared/utils/classNames';
import { Switch } from '~/shared/components/ui/Switch';
import type { UserProfile } from '~/settings/core/types';
import { isMac } from '~/shared/utils/os';
import { logger, type DebugLevel } from '~/shared/utils/logger';
// Helper to get modifier key symbols/text
const getModifierSymbol = (modifier: string): string => {
@@ -24,13 +25,21 @@ export default function SettingsTab() {
const [currentTimezone, setCurrentTimezone] = useState('');
const [settings, setSettings] = useState<UserProfile>(() => {
const saved = localStorage.getItem('bolt_user_profile');
return saved
const profile = saved
? JSON.parse(saved)
: {
notifications: true,
language: 'en',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
logLevel: 'info' as DebugLevel,
};
// Initialize logger with saved preference
if (profile.logLevel) {
logger.setLevel(profile.logLevel);
}
return profile;
});
useEffect(() => {
@@ -49,9 +58,16 @@ export default function SettingsTab() {
notifications: settings.notifications,
language: settings.language,
timezone: settings.timezone,
logLevel: settings.logLevel,
};
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
// Update logger level when logLevel changes
if (settings.logLevel) {
logger.setLevel(settings.logLevel);
}
toast.success('Settings updated');
} catch (error) {
console.error('Error saving settings:', error);
@@ -139,6 +155,50 @@ export default function SettingsTab() {
/>
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:bug-fill w-4 h-4 text-bolt-elements-textSecondary" />
<label className="block text-sm text-bolt-elements-textSecondary">Logging Level</label>
</div>
<select
value={settings.logLevel || 'info'}
onChange={(e) => {
const newLogLevel = e.target.value as DebugLevel;
setSettings((prev) => ({ ...prev, logLevel: newLogLevel }));
// Update logger immediately
logger.setLevel(newLogLevel);
// Update localStorage immediately
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
const updatedProfile = {
...existingProfile,
logLevel: newLogLevel,
};
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
toast.success(`Logging level set to ${newLogLevel}`);
}}
className={classNames(
'w-full px-3 py-2 rounded-lg text-sm',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'transition-all duration-200',
)}
>
<option value="none">None - Disable all logging</option>
<option value="error">Error - Only critical errors</option>
<option value="warn">Warning - Errors and warnings</option>
<option value="info">Info - General information (default)</option>
<option value="debug">Debug - Detailed debugging info</option>
<option value="trace">Trace - Very detailed tracing</option>
</select>
<p className="text-xs text-bolt-elements-textSecondary mt-1">
Controls the verbosity of application logs in the browser console
</p>
</div>
</motion.div>
{/* Timezone */}

View File

@@ -1,4 +1,4 @@
export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
export type DebugLevel = 'none' | 'trace' | 'debug' | 'info' | 'warn' | 'error';
import { Chalk } from 'chalk';
const chalk = new Chalk({ level: 3 });
@@ -14,7 +14,7 @@ interface Logger {
setLevel: (level: DebugLevel) => void;
}
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'none' : 'info';
export const logger: Logger = {
trace: (...messages: any[]) => log('trace', undefined, messages),
@@ -37,15 +37,16 @@ export function createScopedLogger(scope: string): Logger {
}
function setLevel(level: DebugLevel) {
if ((level === 'trace' || level === 'debug') && import.meta.env.PROD) {
return;
}
currentLevel = level;
}
function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];
// If logging is completely disabled, return early
if (currentLevel === 'none') {
return;
}
const levelOrder: DebugLevel[] = ['none', 'trace', 'debug', 'info', 'warn', 'error'];
if (levelOrder.indexOf(level) < levelOrder.indexOf(currentLevel)) {
return;
@@ -98,6 +99,9 @@ function getLabelStyles(color: string, textColor: string) {
function getColorForLevel(level: DebugLevel): string {
switch (level) {
case 'none': {
return '#000000';
}
case 'trace':
case 'debug': {
return '#77828D';

View File

@@ -24,7 +24,6 @@ import { Preview } from './Preview';
import useViewport from '~/shared/hooks';
import { PushToGitHubDialog } from '~/settings/tabs/connections/components/PushToGitHubDialog';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { usePreviewStore } from '~/shared/workbench/stores/previews';
import { chatStore } from '~/chat/stores/chat';
import type { ElementInfo } from './ui/Inspector';
@@ -327,16 +326,9 @@ export const Workbench = memo(
}, []);
const onFileSave = useCallback(() => {
workbenchStore
.saveCurrentDocument()
.then(() => {
// Explicitly refresh all previews after a file save
const previewStore = usePreviewStore();
previewStore.refreshAllPreviews();
})
.catch(() => {
toast.error('Failed to update file content');
});
workbenchStore.saveCurrentDocument().catch(() => {
toast.error('Failed to update file content');
});
}, []);
const onFileReset = useCallback(() => {

View File

@@ -14,182 +14,28 @@ export interface PreviewInfo {
baseUrl: string;
}
// Create a broadcast channel for preview updates
const PREVIEW_CHANNEL = 'preview-updates';
export class PreviewsStore {
#availablePreviews = new Map<number, PreviewInfo>();
#webcontainer: Promise<WebContainer>;
#broadcastChannel: BroadcastChannel;
#lastUpdate = new Map<string, number>();
#watchedFiles = new Set<string>();
#refreshTimeouts = new Map<string, NodeJS.Timeout>();
#REFRESH_DELAY = 300;
#storageChannel: BroadcastChannel;
previews = atom<PreviewInfo[]>([]);
constructor(webcontainerPromise: Promise<WebContainer>) {
this.#webcontainer = webcontainerPromise;
this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL);
this.#storageChannel = new BroadcastChannel('storage-sync-channel');
// Listen for preview updates from other tabs
this.#broadcastChannel.onmessage = (event) => {
const { type, previewId } = event.data;
if (type === 'file-change') {
const timestamp = event.data.timestamp;
const lastUpdate = this.#lastUpdate.get(previewId) || 0;
if (timestamp > lastUpdate) {
this.#lastUpdate.set(previewId, timestamp);
this.refreshPreview(previewId);
}
}
};
// Listen for storage sync messages
this.#storageChannel.onmessage = (event) => {
const { storage, source } = event.data;
if (storage && source !== this._getTabId()) {
this._syncStorage(storage);
}
};
// Override localStorage setItem to catch all changes
if (typeof window !== 'undefined') {
const originalSetItem = localStorage.setItem;
localStorage.setItem = (...args) => {
originalSetItem.apply(localStorage, args);
this._broadcastStorageSync();
};
}
this.#init();
}
// Generate a unique ID for this tab
private _getTabId(): string {
if (typeof window !== 'undefined') {
if (!window._tabId) {
window._tabId = Math.random().toString(36).substring(2, 15);
}
return window._tabId;
}
return '';
}
// Sync storage data between tabs
private _syncStorage(storage: Record<string, string>) {
if (typeof window !== 'undefined') {
Object.entries(storage).forEach(([key, value]) => {
try {
const originalSetItem = Object.getPrototypeOf(localStorage).setItem;
originalSetItem.call(localStorage, key, value);
} catch (error) {
console.error('[Preview] Error syncing storage:', error);
}
});
// Force a refresh after syncing storage
const previews = this.previews.get();
previews.forEach((preview) => {
const previewId = this.getPreviewId(preview.baseUrl);
if (previewId) {
this.refreshPreview(previewId);
}
});
// Reload the page content
if (typeof window !== 'undefined' && window.location) {
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.src = iframe.src;
}
}
}
}
// Broadcast storage state to other tabs
private _broadcastStorageSync() {
if (typeof window !== 'undefined') {
const storage: Record<string, string> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
storage[key] = localStorage.getItem(key) || '';
}
}
this.#storageChannel.postMessage({
type: 'storage-sync',
storage,
source: this._getTabId(),
timestamp: Date.now(),
});
}
}
async #init() {
const webcontainer = await this.#webcontainer;
// Listen for server ready events
webcontainer.on('server-ready', (port, url) => {
console.log('[Preview] Server ready on port:', port, url);
this.broadcastUpdate(url);
// Initial storage sync when preview is ready
this._broadcastStorageSync();
});
try {
// Watch for file changes
webcontainer.internal.watchPaths(
{
// Only watch specific file types that affect the preview
include: ['**/*.html', '**/*.css', '**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.json'],
exclude: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/coverage/**'],
},
async (_events) => {
const previews = this.previews.get();
for (const preview of previews) {
const previewId = this.getPreviewId(preview.baseUrl);
if (previewId) {
this.broadcastFileChange(previewId);
}
}
},
);
// Watch for DOM changes that might affect storage
if (typeof window !== 'undefined') {
const observer = new MutationObserver((_mutations) => {
// Broadcast storage changes when DOM changes
this._broadcastStorageSync();
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
}
} catch (error) {
console.error('[Preview] Error setting up watchers:', error);
}
// Listen for port events
webcontainer.on('port', (port, type, url) => {
let previewInfo = this.#availablePreviews.get(port);
@@ -213,10 +59,6 @@ export class PreviewsStore {
previewInfo.baseUrl = url;
this.previews.set([...previews]);
if (type === 'open') {
this.broadcastUpdate(url);
}
});
}
@@ -226,46 +68,6 @@ export class PreviewsStore {
return match ? match[1] : null;
}
// Broadcast state change to all tabs
broadcastStateChange(previewId: string) {
const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({
type: 'state-change',
previewId,
timestamp,
});
}
// Broadcast file change to all tabs
broadcastFileChange(previewId: string) {
const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({
type: 'file-change',
previewId,
timestamp,
});
}
// Broadcast update to all tabs
broadcastUpdate(url: string) {
const previewId = this.getPreviewId(url);
if (previewId) {
const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({
type: 'file-change',
previewId,
timestamp,
});
}
}
// Method to refresh a specific preview
refreshPreview(previewId: string) {
// Clear any pending refresh for this preview
@@ -295,18 +97,6 @@ export class PreviewsStore {
this.#refreshTimeouts.set(previewId, timeout);
}
refreshAllPreviews() {
const previews = this.previews.get();
for (const preview of previews) {
const previewId = this.getPreviewId(preview.baseUrl);
if (previewId) {
this.broadcastFileChange(previewId);
}
}
}
}
// Create a singleton instance

View File

@@ -579,10 +579,6 @@ export class WorkbenchStore {
this.#editorStore.updateFile(fullPath, data.action.content);
if (!isStreaming && data.action.content) {
await this.saveFile(fullPath);
}
if (!isStreaming) {
await artifact.runner.runAction(data);
this.resetAllFileModifications();

View File

@@ -97,7 +97,7 @@
"@types/react-beautiful-dnd": "^13.1.8",
"@uiw/codemirror-theme-vscode": "^4.23.6",
"@unocss/reset": "^0.61.9",
"@webcontainer/api": "1.6.1-internal.1",
"@webcontainer/api": "1.6.4-internal.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",

10
pnpm-lock.yaml generated
View File

@@ -171,8 +171,8 @@ importers:
specifier: ^0.61.9
version: 0.61.9
'@webcontainer/api':
specifier: 1.6.1-internal.1
version: 1.6.1-internal.1
specifier: 1.6.4-internal.2
version: 1.6.4-internal.2
'@xterm/addon-fit':
specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -3515,8 +3515,8 @@ packages:
'@web3-storage/multipart-parser@1.0.0':
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
'@webcontainer/api@1.6.1-internal.1':
resolution: {integrity: sha512-3kIlLzJonzKlPzONuKudMWHICqwkwyXSsIoUHt4vJwjAr8/qFjlMVxq8ggHS6Yck9JBh1JllDWfCiC95ipSsxw==}
'@webcontainer/api@1.6.4-internal.2':
resolution: {integrity: sha512-ATzYHu8TyuNXOZ+U+Wh1LLLzWY2gbBINmEXnU9lNEMN7E9IA+EEdpsYca1Or0v8rm2cdxrIARuwbm+ffJdrV3w==}
'@xmldom/xmldom@0.8.10':
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
@@ -11658,7 +11658,7 @@ snapshots:
'@web3-storage/multipart-parser@1.0.0': {}
'@webcontainer/api@1.6.1-internal.1': {}
'@webcontainer/api@1.6.4-internal.2': {}
'@xmldom/xmldom@0.8.10': {}