feat: logging to include categories for better log management

This commit is contained in:
Linus Rath
2026-03-31 16:31:27 +02:00
parent dab3606b04
commit 34dd5122b3
17 changed files with 289 additions and 194 deletions
+4 -4
View File
@@ -393,13 +393,13 @@ export const useAuthStore = create<AuthState>()(
oauthAccessToken = access_token;
oauthExpiresIn = expires_in;
upgradedToOAuth = true;
debug.log('TOTP login upgraded to token-based auth (has_refresh_token=' + has_refresh_token + ')');
debug.log('auth', 'TOTP login upgraded to token-based auth (has_refresh_token=' + has_refresh_token + ')');
} else {
const errorBody = await tokenRes.json().catch(() => ({ error: 'unknown' }));
debug.warn('TOTP token exchange failed:', tokenRes.status, errorBody);
debug.warn('auth', 'TOTP token exchange failed:', tokenRes.status, errorBody);
}
} catch (err) {
debug.warn('TOTP token exchange error:', err);
debug.warn('auth', 'TOTP token exchange error:', err);
}
// If token exchange failed, enable TOTP re-auth prompt so the
@@ -407,7 +407,7 @@ export const useAuthStore = create<AuthState>()(
if (!upgradedToOAuth) {
const { useTotpReauthStore } = await import('@/stores/totp-reauth-store');
client.enableTotpReauth(password, () => useTotpReauthStore.getState().requestTotp());
debug.log('TOTP re-auth enabled — user will be prompted for fresh codes on session expiry');
debug.log('auth', 'TOTP re-auth enabled — user will be prompted for fresh codes on session expiry');
}
}
+17 -17
View File
@@ -197,7 +197,7 @@ export const useCalendarStore = create<CalendarStore>()(
// Expand recurring events client-side (Stalwart doesn't support
// mutations on synthetic IDs from server-side expandRecurrences)
const events = expandRecurringEvents(validEvents, start, end);
debug.log('Calendar fetchEvents completed', {
debug.log('calendar', 'Calendar fetchEvents completed', {
start,
end,
rawCount: rawEvents.length,
@@ -206,7 +206,7 @@ export const useCalendarStore = create<CalendarStore>()(
droppedEvents,
});
if (droppedEvents > 0) {
debug.warn('Calendar fetchEvents dropped malformed events without a start field', { droppedEvents });
debug.warn('calendar', 'Calendar fetchEvents dropped malformed events without a start field', { droppedEvents });
}
set({ events, isLoadingEvents: false, dateRange: { start, end } });
} catch (error) {
@@ -237,7 +237,7 @@ export const useCalendarStore = create<CalendarStore>()(
if (event.originalCalendarIds) {
cleanEvent.calendarIds = event.originalCalendarIds;
}
debug.log('Calendar createEvent request', {
debug.log('calendar', 'Calendar createEvent request', {
event: getStoreEventDebugSnapshot(cleanEvent),
sendSchedulingMessages,
targetAccountId,
@@ -256,7 +256,7 @@ export const useCalendarStore = create<CalendarStore>()(
? mappedCreated.start >= currentDateRange.start && mappedCreated.start <= currentDateRange.end
: null;
debug.log('Calendar createEvent response', {
debug.log('calendar', 'Calendar createEvent response', {
created: getStoreEventDebugSnapshot(created),
mappedCreated: getStoreEventDebugSnapshot(mappedCreated),
isVisible,
@@ -265,21 +265,21 @@ export const useCalendarStore = create<CalendarStore>()(
});
if (!isVisible) {
debug.warn('Created event is hidden by current calendar filters', {
debug.warn('calendar', 'Created event is hidden by current calendar filters', {
selectedCalendarIds,
createdCalendarIds,
});
}
if (inCurrentDateRange === false) {
debug.warn('Created event is outside the currently loaded date range', {
debug.warn('calendar', 'Created event is outside the currently loaded date range', {
currentDateRange,
createdStart: mappedCreated.start,
});
}
if (mappedCreated.showWithoutTime && mappedCreated.timeZone !== null) {
debug.warn('Created all-day event came back with a non-null timeZone', {
debug.warn('calendar', 'Created all-day event came back with a non-null timeZone', {
timeZone: mappedCreated.timeZone,
event: getStoreEventDebugSnapshot(mappedCreated),
});
@@ -301,7 +301,7 @@ export const useCalendarStore = create<CalendarStore>()(
const storeEvent = get().events.find(e => e.id === id);
const realId = storeEvent?.originalId || id;
const targetAccountId = storeEvent?.accountId;
debug.log('Calendar updateEvent', {
debug.log('calendar', 'Calendar updateEvent', {
storeId: id,
realId,
uid: storeEvent?.uid,
@@ -445,20 +445,20 @@ export const useCalendarStore = create<CalendarStore>()(
await client.updateCalendarEvent(eventId, { calendarIds } as Partial<CalendarEvent>, undefined, targetAccountId);
linked++;
} catch (err) {
debug.warn(`Import: failed to link event ${eventId} to target calendar:`, err);
debug.warn('calendar', `Import: failed to link event ${eventId} to target calendar:`, err);
}
}
if (linked > 0) {
debug.log(`Import: linked ${linked} existing events to target calendar`);
debug.log('calendar', `Import: linked ${linked} existing events to target calendar`);
}
const skipped = eventsToProcess.length - newEvents.length - eventsToLink.length;
if (skipped > 0) {
debug.log(`Import: skipped ${skipped} events already in target calendar`);
debug.log('calendar', `Import: skipped ${skipped} events already in target calendar`);
}
eventsToProcess = newEvents;
} catch (error) {
debug.warn('Could not fetch existing events for deduplication, proceeding without:', error);
debug.warn('calendar', 'Could not fetch existing events for deduplication, proceeding without:', error);
}
// Prepare all events for batch creation
@@ -543,7 +543,7 @@ export const useCalendarStore = create<CalendarStore>()(
const { created, failed } = await client.batchCreateCalendarEvents(batch, targetAccountId);
imported += created.length;
if (failed.length > 0) {
debug.warn(`Import batch ${i / BATCH_SIZE + 1}: ${failed.length} events failed`);
debug.warn('calendar', `Import batch ${i / BATCH_SIZE + 1}: ${failed.length} events failed`);
}
} catch (error) {
debug.error(`Import batch ${i / BATCH_SIZE + 1} failed:`, error);
@@ -577,7 +577,7 @@ export const useCalendarStore = create<CalendarStore>()(
debug.error('Failed to send cancellation emails:', e);
}
}
debug.log('Calendar deleteEvent', {
debug.log('calendar', 'Calendar deleteEvent', {
storeId: id,
realId,
uid: storeEvent?.uid,
@@ -675,7 +675,7 @@ export const useCalendarStore = create<CalendarStore>()(
// If we couldn't destroy any events, stop to avoid infinite loop
if (destroyed.length === 0) {
debug.warn('Could not delete any events, stopping clear loop. Not destroyed:', ids.length);
debug.warn('calendar', 'Could not delete any events, stopping clear loop. Not destroyed:', ids.length);
break;
}
@@ -744,7 +744,7 @@ export const useCalendarStore = create<CalendarStore>()(
await get().refreshICalSubscription(client, subscription.id);
} catch {
// Subscription created, initial fetch failed - user can retry
debug.warn('Initial subscription fetch failed for:', name);
debug.warn('calendar', 'Initial subscription fetch failed for:', name);
}
return subscription;
@@ -892,7 +892,7 @@ export const useCalendarStore = create<CalendarStore>()(
try {
await get().refreshICalSubscription(client, sub.id);
} catch {
debug.warn('Failed to refresh subscription:', sub.name);
debug.warn('calendar', 'Failed to refresh subscription:', sub.name);
}
}
}
+5 -5
View File
@@ -53,7 +53,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
set({ sieveCapabilities: capabilities });
const allScripts = await client.getSieveScripts();
debug.log('Sieve scripts fetched:', allScripts.length);
debug.log('filters', 'Sieve scripts fetched:', allScripts.length);
// Skip the server-managed 'vacation' script (RFC 9661 §4) — it can only
// be modified via VacationResponse/set, not SieveScript/set.
@@ -73,10 +73,10 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
const result = parseScript(content);
if (result.isOpaque) {
debug.log('Sieve script is opaque (hand-edited)');
debug.log('filters', 'Sieve script is opaque (hand-edited)');
set({ isLoading: false, isOpaque: true, rules: [], vacationSettings: result.vacation || null });
} else {
debug.log('Parsed', result.rules.length, 'filter rules');
debug.log('filters', 'Parsed', result.rules.length, 'filter rules');
set({ isLoading: false, isOpaque: false, rules: result.rules, vacationSettings: result.vacation || null });
}
} catch (error) {
@@ -108,7 +108,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
}
set({ isSaving: false, rawScript: content });
debug.log('Filters saved successfully');
debug.log('filters', 'Filters saved successfully');
} catch (error) {
debug.error('Failed to save filters:', error);
set({
@@ -212,7 +212,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
});
}
debug.log('Vacation synced to sieve script');
debug.log('filters', 'Vacation synced to sieve script');
} catch (error) {
debug.error('Failed to sync vacation to sieve script:', error);
}
+23
View File
@@ -49,6 +49,18 @@ export const ALL_HOVER_ACTIONS: { id: HoverAction; labelKey: string }[] = [
{ id: 'spam', labelKey: 'spam' },
];
export type DebugCategory = 'jmap' | 'calendar' | 'tasks' | 'auth' | 'filters' | 'email' | 'push';
export const ALL_DEBUG_CATEGORIES: { id: DebugCategory; labelKey: string }[] = [
{ id: 'jmap', labelKey: 'jmap' },
{ id: 'calendar', labelKey: 'calendar' },
{ id: 'tasks', labelKey: 'tasks' },
{ id: 'auth', labelKey: 'auth' },
{ id: 'filters', labelKey: 'filters' },
{ id: 'email', labelKey: 'email' },
{ id: 'push', labelKey: 'push' },
];
export interface KeywordDefinition {
id: string; // Used as JMAP keyword suffix: $label:<id>
label: string; // Display name
@@ -175,6 +187,7 @@ interface SettingsState {
// Advanced
debugMode: boolean;
debugCategories: Record<DebugCategory, boolean>;
settingsSyncDisabled: boolean;
// Actions
@@ -299,6 +312,15 @@ const DEFAULT_SETTINGS = {
// Advanced
debugMode: false,
debugCategories: {
jmap: true,
calendar: true,
tasks: true,
auth: true,
filters: true,
email: true,
push: true,
} as Record<DebugCategory, boolean>,
settingsSyncDisabled: false,
};
@@ -383,6 +405,7 @@ export const useSettingsStore = create<SettingsState>()(
sidebarApps: state.sidebarApps,
keepAppsLoaded: state.keepAppsLoaded,
debugMode: state.debugMode,
debugCategories: state.debugCategories,
settingsSyncDisabled: state.settingsSyncDisabled,
// Cross-store settings
theme: useThemeStore.getState().theme,
+5 -5
View File
@@ -37,13 +37,13 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
setShowCompleted: (show) => set({ showCompleted: show }),
fetchTasks: async (client, calendarIds) => {
debug.log('TaskStore/fetchTasks start', { calendarIds: calendarIds || 'all' });
debug.log('tasks', 'TaskStore/fetchTasks start', { calendarIds: calendarIds || 'all' });
set({ isLoading: true, error: null });
try {
const tasks = await client.getCalendarTasks(calendarIds);
debug.log('TaskStore/fetchTasks received', tasks.length, 'tasks');
debug.log('tasks', 'TaskStore/fetchTasks received', tasks.length, 'tasks');
tasks.forEach((t, i) => {
debug.log(`TaskStore/fetchTasks [${i}]`, {
debug.log('tasks', `TaskStore/fetchTasks [${i}]`, {
id: t.id, uid: t.uid, '@type': t['@type'],
title: t.title, due: t.due, progress: t.progress,
showWithoutTime: t.showWithoutTime, calendarIds: t.calendarIds,
@@ -57,9 +57,9 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
},
createTask: async (client, task) => {
debug.log('TaskStore/createTask', task);
debug.log('tasks', 'TaskStore/createTask', task);
const created = await client.createCalendarTask(task);
debug.log('TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
debug.log('tasks', 'TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
set({ tasks: [...get().tasks, created] });
return created;
},