Fix frontend log streaming in no-auth mode (#130)

This commit is contained in:
Peter Steinberger 2025-06-29 18:41:40 +01:00 committed by GitHub
parent 099a5fb427
commit 39a5933f9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 489 additions and 54 deletions

View file

@ -167,10 +167,10 @@ export class VibeTunnelApp extends LitElement {
const configResponse = await fetch('/api/auth/config');
if (configResponse.ok) {
const authConfig = await configResponse.json();
console.log('🔧 Auth config:', authConfig);
logger.log('🔧 Auth config:', authConfig);
if (authConfig.noAuth) {
console.log('🔓 No auth required, bypassing authentication');
logger.log('🔓 No auth required, bypassing authentication');
this.isAuthenticated = true;
this.currentView = 'list';
await this.initializeServices(); // Initialize services after auth
@ -180,11 +180,11 @@ export class VibeTunnelApp extends LitElement {
}
}
} catch (error) {
console.warn('⚠️ Could not fetch auth config:', error);
logger.warn('⚠️ Could not fetch auth config:', error);
}
this.isAuthenticated = authClient.isAuthenticated();
console.log('🔐 Authentication status:', this.isAuthenticated);
logger.log('🔐 Authentication status:', this.isAuthenticated);
if (this.isAuthenticated) {
this.currentView = 'list';
@ -197,7 +197,7 @@ export class VibeTunnelApp extends LitElement {
}
private async handleAuthSuccess() {
console.log('✅ Authentication successful');
logger.log('✅ Authentication successful');
this.isAuthenticated = true;
this.currentView = 'list';
await this.initializeServices(); // Initialize services after auth
@ -219,7 +219,7 @@ export class VibeTunnelApp extends LitElement {
}
private async initializeServices() {
console.log('🚀 Initializing services...');
logger.log('🚀 Initializing services...');
try {
// Initialize buffer subscription service for WebSocket connections
await bufferSubscriptionService.initialize();
@ -227,16 +227,16 @@ export class VibeTunnelApp extends LitElement {
// Initialize push notification service
await pushNotificationService.initialize();
console.log('✅ Services initialized successfully');
logger.log('✅ Services initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize services:', error);
logger.error('❌ Failed to initialize services:', error);
// Don't fail the whole app if services fail to initialize
// These are optional features
}
}
private async handleLogout() {
console.log('👋 Logging out');
logger.log('👋 Logging out');
await authClient.logout();
this.isAuthenticated = false;
this.currentView = 'auth';
@ -316,7 +316,7 @@ export class VibeTunnelApp extends LitElement {
const sessionExists = this.sessions.find((s) => s.id === this.selectedSessionId);
if (!sessionExists) {
// Session no longer exists, redirect to dashboard
console.warn(
logger.warn(
`Selected session ${this.selectedSessionId} no longer exists, redirecting to dashboard`
);
this.selectedSessionId = null;
@ -474,7 +474,7 @@ export class VibeTunnelApp extends LitElement {
}
private async handleHideExitedChange(e: CustomEvent) {
console.log('handleHideExitedChange', {
logger.log('handleHideExitedChange', {
currentHideExited: this.hideExited,
newHideExited: e.detail,
});
@ -491,7 +491,7 @@ export class VibeTunnelApp extends LitElement {
// Add pre-animation class
document.body.classList.add('sessions-animating');
console.log('Added sessions-animating class');
logger.log('Added sessions-animating class');
// Update state
this.hideExited = e.detail;
@ -499,17 +499,17 @@ export class VibeTunnelApp extends LitElement {
// Wait for render and trigger animations
await this.updateComplete;
console.log('Update complete, scheduling animation');
logger.log('Update complete, scheduling animation');
requestAnimationFrame(() => {
// Add specific animation direction
const animationClass = wasHidingExited ? 'sessions-showing' : 'sessions-hiding';
document.body.classList.add(animationClass);
console.log('Added animation class:', animationClass);
logger.log('Added animation class:', animationClass);
// Check what elements will be animated
const cards = document.querySelectorAll('.session-flex-responsive > session-card');
console.log('Found session cards to animate:', cards.length);
logger.log('Found session cards to animate:', cards.length);
// If we were near the bottom, maintain that position
if (isNearBottom) {
@ -525,7 +525,7 @@ export class VibeTunnelApp extends LitElement {
// Clean up after animation
setTimeout(() => {
document.body.classList.remove('sessions-animating', 'sessions-showing', 'sessions-hiding');
console.log('Cleaned up animation classes');
logger.log('Cleaned up animation classes');
// Final scroll adjustment after animation completes
if (isNearBottom) {
@ -890,12 +890,12 @@ export class VibeTunnelApp extends LitElement {
window.addEventListener('popstate', this.handlePopState.bind(this));
// Parse initial URL and set state
this.parseUrlAndSetState().catch(console.error);
this.parseUrlAndSetState().catch((error) => logger.error('Error parsing URL:', error));
}
private handlePopState = (_event: PopStateEvent) => {
// Handle browser back/forward navigation
this.parseUrlAndSetState().catch(console.error);
this.parseUrlAndSetState().catch((error) => logger.error('Error parsing URL:', error));
};
private async parseUrlAndSetState() {
@ -941,7 +941,7 @@ export class VibeTunnelApp extends LitElement {
this.currentView = 'session';
} else {
// Session not found, go to list view
console.warn(`Session ${sessionId} not found in sessions list`);
logger.warn(`Session ${sessionId} not found in sessions list`);
this.selectedSessionId = null;
this.currentView = 'list';
// Clear the session param from URL
@ -1000,7 +1000,7 @@ export class VibeTunnelApp extends LitElement {
this.showLogLink = preferences.showLogLink || false;
}
} catch (error) {
console.error('Failed to load app preferences', error);
logger.error('Failed to load app preferences', error);
}
// Listen for preference changes
@ -1011,7 +1011,7 @@ export class VibeTunnelApp extends LitElement {
}
private handleOpenSettings = () => {
console.log('🎯 handleOpenSettings called in app.ts');
logger.log('🎯 handleOpenSettings called in app.ts');
this.showSettings = true;
};

View file

@ -311,7 +311,9 @@ describe('SessionCard', () => {
// Should use cleanup endpoint for exited sessions
const calls = fetchMock.getCalls();
expect(calls[0][0]).toContain('/cleanup');
const cleanupCall = calls.find((call) => call[0].includes('/cleanup'));
expect(cleanupCall).toBeDefined();
expect(cleanupCall?.[0]).toContain('/cleanup');
expect(killedHandler).toHaveBeenCalled();
});
});

View file

@ -329,7 +329,7 @@ export class SessionView extends LitElement {
this.useDirectKeyboard = true; // Default to true when no settings exist
}
} catch (error) {
console.error('Failed to load app preferences', error);
logger.error('Failed to load app preferences', error);
this.useDirectKeyboard = true; // Default to true on error
}

View file

@ -1,5 +1,8 @@
import { createLogger } from '../utils/logger.js';
import { BrowserSSHAgent } from './ssh-agent.js';
const logger = createLogger('auth-client');
interface AuthResponse {
success: boolean;
token?: string;
@ -66,7 +69,7 @@ export class AuthClient {
}
throw new Error('Failed to get current user');
} catch (error) {
console.error('Failed to get current system user:', error);
logger.error('Failed to get current system user:', error);
throw error;
}
}
@ -90,7 +93,7 @@ export class AuthClient {
}
}
} catch (error) {
console.error('Failed to get user avatar:', error);
logger.error('Failed to get user avatar:', error);
}
// Return generic avatar SVG for non-macOS or when no avatar found
@ -139,24 +142,24 @@ export class AuthClient {
});
const result = await response.json();
console.log('🔐 SSH key auth server response:', result);
logger.log('🔐 SSH key auth server response:', result);
if (result.success) {
console.log('✅ SSH key auth successful, setting current user');
logger.log('✅ SSH key auth successful, setting current user');
this.setCurrentUser({
userId: result.userId,
token: result.token,
authMethod: 'ssh-key',
loginTime: Date.now(),
});
console.log('👤 Current user set:', this.getCurrentUser());
logger.log('👤 Current user set:', this.getCurrentUser());
} else {
console.log('❌ SSH key auth failed:', result.error);
logger.log('❌ SSH key auth failed:', result.error);
}
return result;
} catch (error) {
console.error('SSH key authentication failed:', error);
logger.error('SSH key authentication failed:', error);
return { success: false, error: 'SSH key authentication failed' };
}
}
@ -185,7 +188,7 @@ export class AuthClient {
return result;
} catch (error) {
console.error('Password authentication failed:', error);
logger.error('Password authentication failed:', error);
return { success: false, error: 'Password authentication failed' };
}
}
@ -194,12 +197,12 @@ export class AuthClient {
* Automated authentication - tries SSH keys first, then prompts for password
*/
async authenticate(userId: string): Promise<AuthResponse> {
console.log('🚀 Starting SSH authentication for user:', userId);
logger.log('🚀 Starting SSH authentication for user:', userId);
// Try SSH key authentication first if agent is unlocked
if (this.sshAgent.isUnlocked()) {
const keys = this.sshAgent.listKeys();
console.log(
logger.log(
'🗝️ Found SSH keys:',
keys.length,
keys.map((k) => ({ id: k.id, name: k.name }))
@ -207,20 +210,20 @@ export class AuthClient {
for (const key of keys) {
try {
console.log(`🔑 Trying SSH key: ${key.name} (${key.id})`);
logger.log(`🔑 Trying SSH key: ${key.name} (${key.id})`);
const result = await this.authenticateWithSSHKey(userId, key.id);
console.log(`🎯 SSH key ${key.name} result:`, result);
logger.log(`🎯 SSH key ${key.name} result:`, result);
if (result.success) {
console.log(`✅ Authenticated with SSH key: ${key.name}`);
logger.log(`✅ Authenticated with SSH key: ${key.name}`);
return result;
}
} catch (error) {
console.warn(`❌ SSH key authentication failed for key ${key.name}:`, error);
logger.warn(`❌ SSH key authentication failed for key ${key.name}:`, error);
}
}
} else {
console.log('🔒 SSH agent is locked');
logger.log('🔒 SSH agent is locked');
}
// SSH key auth failed or no keys available
@ -246,7 +249,7 @@ export class AuthClient {
});
}
} catch (error) {
console.warn('Server logout failed:', error);
logger.warn('Server logout failed:', error);
} finally {
// Clear local state
this.clearCurrentUser();
@ -260,10 +263,7 @@ export class AuthClient {
if (this.currentUser?.token) {
return { Authorization: `Bearer ${this.currentUser.token}` };
}
// Suppress warning in test environment to reduce noise
if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'test') {
console.warn('⚠️ No token available for auth header');
}
// No warning needed when token is not available
return {};
}
@ -281,7 +281,7 @@ export class AuthClient {
const result = await response.json();
return result.valid;
} catch (error) {
console.error('Token verification failed:', error);
logger.error('Token verification failed:', error);
return false;
}
}
@ -370,7 +370,7 @@ export class AuthClient {
});
}
} catch (error) {
console.error('Failed to load current user:', error);
logger.error('Failed to load current user:', error);
this.clearCurrentUser();
}
}

View file

@ -0,0 +1,379 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { clearAuthConfigCache, createLogger, setDebugMode } from './logger.js';
// Mock the auth client module
vi.mock('../services/auth-client.js', () => ({
authClient: {
getAuthHeader: vi.fn(),
},
}));
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('Frontend Logger', () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let consoleDebugSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();
mockFetch.mockReset();
// Spy on console methods
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
// Reset debug mode
setDebugMode(false);
// Clear auth config cache
clearAuthConfigCache();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('Console Logging', () => {
it('should log to console with module prefix', () => {
const logger = createLogger('test-module');
logger.log('test message');
expect(consoleLogSpy).toHaveBeenCalledWith('[test-module]', 'test message');
logger.warn('warning message');
expect(consoleWarnSpy).toHaveBeenCalledWith('[test-module]', 'warning message');
logger.error('error message');
expect(consoleErrorSpy).toHaveBeenCalledWith('[test-module]', 'error message');
});
it('should not log debug messages when debug mode is disabled', () => {
const logger = createLogger('test-module');
logger.debug('debug message');
expect(consoleDebugSpy).not.toHaveBeenCalled();
});
it('should log debug messages when debug mode is enabled', () => {
setDebugMode(true);
const logger = createLogger('test-module');
logger.debug('debug message');
expect(consoleDebugSpy).toHaveBeenCalledWith('[test-module]', 'debug message');
});
it('should handle multiple arguments', () => {
const logger = createLogger('test-module');
logger.log('message', { data: 'test' }, 123);
expect(consoleLogSpy).toHaveBeenCalledWith('[test-module]', 'message', { data: 'test' }, 123);
});
});
describe('Server Logging - Authenticated Mode', () => {
beforeEach(async () => {
const { authClient } = await import('../services/auth-client.js');
vi.mocked(authClient.getAuthHeader).mockReturnValue({
Authorization: 'Bearer test-token',
});
});
it('should send logs to server when authenticated', async () => {
mockFetch.mockResolvedValueOnce(new Response());
const logger = createLogger('test-module');
logger.log('test message');
// Wait for async operations
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalled());
expect(mockFetch).toHaveBeenCalledWith('/api/logs/client', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({
level: 'log',
module: 'test-module',
args: ['test message'],
}),
});
});
it('should format objects as JSON strings', async () => {
mockFetch.mockResolvedValueOnce(new Response());
const logger = createLogger('test-module');
const testObj = { key: 'value', nested: { data: 123 } };
logger.log('message', testObj);
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalled());
const lastCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1];
const body = JSON.parse(lastCall[1].body);
expect(body.args).toEqual(['message', JSON.stringify(testObj, null, 2)]);
});
it('should handle all log levels', async () => {
mockFetch.mockResolvedValue(new Response());
const logger = createLogger('test-module');
logger.log('log message');
await new Promise((resolve) => setTimeout(resolve, 50));
logger.warn('warn message');
await new Promise((resolve) => setTimeout(resolve, 50));
logger.error('error message');
await new Promise((resolve) => setTimeout(resolve, 50));
const calls = mockFetch.mock.calls.filter((call) => call[0] === '/api/logs/client');
expect(calls).toHaveLength(3);
expect(JSON.parse(calls[0][1].body).level).toBe('log');
expect(JSON.parse(calls[1][1].body).level).toBe('warn');
expect(JSON.parse(calls[2][1].body).level).toBe('error');
});
});
describe('Server Logging - No-Auth Mode', () => {
beforeEach(async () => {
const { authClient } = await import('../services/auth-client.js');
vi.mocked(authClient.getAuthHeader).mockReturnValue({});
// Mock auth config endpoint to return no-auth mode
mockFetch.mockImplementation((url) => {
if (url === '/api/auth/config') {
return Promise.resolve(
new Response(JSON.stringify({ noAuth: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
);
}
return Promise.resolve(new Response());
});
});
it('should send logs to server in no-auth mode without auth header', async () => {
const logger = createLogger('test-module');
logger.log('test message in no-auth');
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)); // auth config + log
// Check auth config was fetched
expect(mockFetch).toHaveBeenCalledWith('/api/auth/config');
// Check log was sent without auth header
const logCall = mockFetch.mock.calls.find((call) => call[0] === '/api/logs/client');
expect(logCall).toBeDefined();
expect(logCall?.[1].headers).toEqual({
'Content-Type': 'application/json',
});
expect(JSON.parse(logCall?.[1].body)).toEqual({
level: 'log',
module: 'test-module',
args: ['test message in no-auth'],
});
});
it('should cache auth config to reduce redundant requests', async () => {
const logger = createLogger('test-module');
logger.log('message 1');
await new Promise((resolve) => setTimeout(resolve, 50));
logger.log('message 2');
await new Promise((resolve) => setTimeout(resolve, 50));
logger.log('message 3');
await new Promise((resolve) => setTimeout(resolve, 50));
// The logger should only check auth config once due to caching
const authConfigCalls = mockFetch.mock.calls.filter((call) => call[0] === '/api/auth/config');
expect(authConfigCalls).toHaveLength(1);
// Should have sent all 3 log messages
const logCalls = mockFetch.mock.calls.filter((call) => call[0] === '/api/logs/client');
expect(logCalls).toHaveLength(3);
});
it('should refetch auth config after cache expires', async () => {
const logger = createLogger('test-module');
// First log - should fetch auth config
logger.log('message 1');
await new Promise((resolve) => setTimeout(resolve, 50));
// Clear cache to simulate expiration
clearAuthConfigCache();
// Second log - should fetch auth config again
logger.log('message 2');
await new Promise((resolve) => setTimeout(resolve, 50));
// Should have fetched auth config twice
const authConfigCalls = mockFetch.mock.calls.filter((call) => call[0] === '/api/auth/config');
expect(authConfigCalls).toHaveLength(2);
});
});
describe('Server Logging - Not Authenticated', () => {
beforeEach(async () => {
const { authClient } = await import('../services/auth-client.js');
vi.mocked(authClient.getAuthHeader).mockReturnValue({});
// Mock auth config endpoint to return auth required
mockFetch.mockImplementation((url) => {
if (url === '/api/auth/config') {
return Promise.resolve(
new Response(JSON.stringify({ noAuth: false }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
);
}
return Promise.resolve(new Response());
});
});
it('should not send logs when not authenticated and auth is required', async () => {
const logger = createLogger('test-module');
logger.log('test message');
// Wait a bit to ensure async operations complete
await new Promise((resolve) => setTimeout(resolve, 100));
// Should only call auth config, not the log endpoint
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith('/api/auth/config');
const logCalls = mockFetch.mock.calls.filter((call) => call[0] === '/api/logs/client');
expect(logCalls).toHaveLength(0);
});
});
describe('Error Handling', () => {
beforeEach(async () => {
const { authClient } = await import('../services/auth-client.js');
vi.mocked(authClient.getAuthHeader).mockReturnValue({
Authorization: 'Bearer test-token',
});
});
it('should silently ignore network errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
const logger = createLogger('test-module');
// Should not throw
expect(() => logger.log('test message')).not.toThrow();
// Console log should still work
expect(consoleLogSpy).toHaveBeenCalledWith('[test-module]', 'test message');
});
it('should handle auth config fetch errors gracefully', async () => {
const { authClient } = await import('../services/auth-client.js');
vi.mocked(authClient.getAuthHeader).mockReturnValue({});
// Make auth config fetch fail
mockFetch.mockRejectedValueOnce(new Error('Config fetch failed'));
const logger = createLogger('test-module');
// Should not throw
expect(() => logger.log('test message')).not.toThrow();
// Console log should still work
expect(consoleLogSpy).toHaveBeenCalledWith('[test-module]', 'test message');
});
it('should handle circular objects gracefully', async () => {
mockFetch.mockResolvedValueOnce(new Response());
const logger = createLogger('test-module');
// Create circular reference
const circular: Record<string, unknown> = { a: 1 };
circular.self = circular;
logger.log('circular', circular);
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalled());
const lastCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1];
const body = JSON.parse(lastCall[1].body);
// Should convert to string when JSON.stringify fails
expect(body.args[1]).toBe('[object Object]');
});
it('should handle non-200 responses from auth config', async () => {
const { authClient } = await import('../services/auth-client.js');
vi.mocked(authClient.getAuthHeader).mockReturnValue({});
// Mock auth config endpoint to return error
mockFetch.mockResolvedValueOnce(
new Response('Unauthorized', {
status: 401,
headers: { 'Content-Type': 'text/plain' },
})
);
const logger = createLogger('test-module');
logger.log('test message');
// Wait a bit
await new Promise((resolve) => setTimeout(resolve, 100));
// Should not send log when auth config fails
const logCalls = mockFetch.mock.calls.filter((call) => call[0] === '/api/logs/client');
expect(logCalls).toHaveLength(0);
});
});
describe('Debug Mode', () => {
beforeEach(async () => {
const { authClient } = await import('../services/auth-client.js');
vi.mocked(authClient.getAuthHeader).mockReturnValue({
Authorization: 'Bearer test-token',
});
mockFetch.mockResolvedValue(new Response());
});
it('should not send debug logs to server when debug mode is disabled', async () => {
setDebugMode(false);
const logger = createLogger('test-module');
logger.debug('debug message');
// Wait a bit
await new Promise((resolve) => setTimeout(resolve, 100));
// Should not call fetch
expect(mockFetch).not.toHaveBeenCalled();
});
it('should send debug logs to server when debug mode is enabled', async () => {
setDebugMode(true);
const logger = createLogger('test-module');
logger.debug('debug message');
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalled());
const lastCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1];
const body = JSON.parse(lastCall[1].body);
expect(body.level).toBe('debug');
expect(body.args).toEqual(['debug message']);
});
});
});

View file

@ -16,13 +16,56 @@ interface Logger {
let debugMode = false;
// Auth config cache to reduce redundant requests
let authConfigCache: { noAuth: boolean; timestamp: number } | null = null;
const AUTH_CONFIG_TTL = 60000; // 1 minute
/**
* Enable or disable debug mode
* Enable or disable debug mode for all loggers
* @param enabled - Whether to enable debug logging
*/
export function setDebugMode(enabled: boolean): void {
debugMode = enabled;
}
/**
* Clear the auth config cache (mainly for testing)
*/
export function clearAuthConfigCache(): void {
authConfigCache = null;
}
/**
* Get cached auth configuration or fetch it
* @returns Whether no-auth mode is enabled
*/
async function getAuthConfig(): Promise<boolean> {
const now = Date.now();
// Return cached value if still valid
if (authConfigCache && now - authConfigCache.timestamp < AUTH_CONFIG_TTL) {
return authConfigCache.noAuth;
}
// Fetch and cache new value
try {
const configResponse = await fetch('/api/auth/config');
if (configResponse.ok) {
const authConfig = await configResponse.json();
authConfigCache = {
noAuth: authConfig.noAuth === true,
timestamp: now,
};
return authConfigCache.noAuth;
}
} catch {
// Ignore auth config fetch errors
}
// Default to false if fetch fails
return false;
}
/**
* Format arguments for consistent logging
*/
@ -50,17 +93,27 @@ async function sendToServer(level: keyof LogLevel, module: string, args: unknown
// Check if we have authentication before sending logs
const authHeader = authClient.getAuthHeader();
if (!authHeader.Authorization) {
// Skip sending logs if not authenticated
// Check if no-auth mode is enabled (cached)
const isNoAuthMode = await getAuthConfig();
// Skip sending logs if not authenticated AND not in no-auth mode
if (!authHeader.Authorization && !isNoAuthMode) {
return;
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// Only add auth header if we have one
if (authHeader.Authorization) {
headers.Authorization = authHeader.Authorization;
}
await fetch('/api/logs/client', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authClient.getAuthHeader(),
},
headers,
body: JSON.stringify({
level,
module,
@ -73,8 +126,9 @@ async function sendToServer(level: keyof LogLevel, module: string, args: unknown
}
/**
* Create a logger for a specific module
* This mirrors the server's createLogger interface
* Creates a logger instance for a specific module
* @param moduleName - The name of the module for log context
* @returns Logger instance with log, warn, error, and debug methods
*/
export function createLogger(moduleName: string): Logger {
const createLogMethod = (level: keyof LogLevel): LogMethod => {