super-productivity/packages/plugin-dev/sync-md/src/background/ui-bridge.ts
Johannes Millan f9fd8454cc fix(sync-md): prevent crash when adding subtasks to markdown file directly
Fixes #6021

When users manually added subtasks to markdown files that referenced
non-existent parent tasks, the plugin would crash due to unsafe null
assertions and missing validation. This made the plugin permanently
disabled and required creating a new markdown file to recover.

Changes:
- Added parent ID validation before creating operations
- Orphaned subtasks are now converted to root tasks with warnings
- Added comprehensive error handling with user notifications
- Plugin stays enabled even after sync errors
- Added 15 new tests (689 lines) for orphaned subtask scenarios
- Fixed all existing tests to support new error notifications

The fix implements defense-in-depth:
1. Validation layer: Check parent IDs exist before operations
2. Error handling: Catch and report errors without crashing
3. User feedback: Clear notifications about issues
4. Data preservation: No data loss, orphans become root tasks
2026-01-16 13:28:05 +01:00

107 lines
3.2 KiB
TypeScript

import { loadLocalConfig, LocalUserCfg, saveLocalConfig } from './local-config';
import { initSyncManager } from './sync/sync-manager';
interface UiBridgeConfig {
getConfig: () => LocalUserCfg | null;
saveConfig: (config: LocalUserCfg) => void;
triggerSync: () => void;
}
interface PluginMessage {
type: string;
config?: LocalUserCfg;
}
const bridgeConfig: UiBridgeConfig = {
getConfig: () => loadLocalConfig(),
saveConfig: (newConfig: LocalUserCfg) => {
console.log('[sync-md] Saving config:', newConfig);
saveLocalConfig(newConfig);
console.log('[sync-md] Config saved to localStorage');
// Re-initialize sync when config changes
if (newConfig.filePath) {
console.log(
'[sync-md] Initializing sync manager with config, filePath:',
newConfig.filePath,
);
try {
initSyncManager(newConfig);
console.log('[sync-md] Sync manager initialized successfully');
} catch (error) {
console.error('[sync-md] Failed to initialize sync manager:', error);
// Show error but DON'T throw - keep plugin running
PluginAPI.showSnack({
msg: 'Sync.md: Failed to initialize. Check file path and permissions.',
type: 'ERROR',
});
}
}
},
triggerSync: () => {
const config = loadLocalConfig();
if (config?.filePath) {
// Transform config to match sync-manager expectations
initSyncManager(config);
}
},
};
export const initUiBridge = (): void => {
console.log(
'[sync-md] initUiBridge called, PluginAPI.onMessage available:',
!!PluginAPI.onMessage,
);
// Handle messages from UI
if (PluginAPI.onMessage) {
console.log('[sync-md] Registering message handler');
PluginAPI.onMessage(async (message: PluginMessage) => {
console.log('[sync-md] Received message:', message);
switch (message.type) {
case 'getProjects':
try {
const projects = await PluginAPI.getAllProjects();
return { success: true, projects };
} catch (error) {
return { success: false, error: (error as Error).message };
}
case 'getConfig':
try {
const config = bridgeConfig.getConfig();
return { success: true, config };
} catch (error) {
return { success: false, error: (error as Error).message };
}
case 'saveConfig':
try {
if (message.config) {
bridgeConfig.saveConfig(message.config);
return { success: true };
}
return { success: false, error: 'No config provided' };
} catch (error) {
return { success: false, error: (error as Error).message };
}
case 'syncNow':
try {
const config = bridgeConfig.getConfig();
if (config?.filePath) {
bridgeConfig.triggerSync();
return { success: true };
}
return { success: false, error: 'Sync not enabled' };
} catch (error) {
return { success: false, error: (error as Error).message };
}
default:
return { success: false, error: 'Unknown message type' };
}
});
}
};