From a3e4fa4ec7358438e1470443424805d142c4302a Mon Sep 17 00:00:00 2001 From: "michael.borak" Date: Sat, 24 Jan 2026 13:10:18 +0100 Subject: [PATCH] Release 1.2.0: Remove backup encryption and switch to JSON-only backups with history support --- src/App.tsx | 113 ++++++++++++++++- src/components/Settings.tsx | 237 +++++++++++++++++++----------------- 2 files changed, 229 insertions(+), 121 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4619c80..a415071 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; +import { invoke } from '@tauri-apps/api/core'; import { Settings as SettingsIcon } from "lucide-react"; import Settings, { SmtpConfig, AzureConfig } from "./components/Settings"; import Recorder from "./components/Recorder"; @@ -60,6 +61,11 @@ function App() { return localStorage.getItem('hearbit_selected_model') || 'mixtral'; }); + // Daily Backup State + const [dailyBackupEnabled, setDailyBackupEnabled] = useState(() => localStorage.getItem('hearbit_daily_backup_enabled') === 'true'); + const [dailyBackupPath, setDailyBackupPath] = useState(() => localStorage.getItem('hearbit_daily_backup_path') || ''); + const [lastBackupDate, setLastBackupDate] = useState(() => localStorage.getItem('hearbit_last_backup_date') || ''); + const handleModelChange = (model: string) => { setSelectedModel(model); localStorage.setItem('hearbit_selected_model', model); @@ -227,6 +233,7 @@ Thanks!` return saved ? JSON.parse(saved) : defaultEmailTemplates; }); + const handleSaveSettings = ( newApiKey: string, newProductId: string, @@ -234,7 +241,9 @@ Thanks!` newSavePath: string, newSmtp: SmtpConfig, newAzure: AzureConfig, - newEmailTemplates: EmailTemplate[] + newEmailTemplates: EmailTemplate[], + newDailyBackupEnabled: boolean, + newDailyBackupPath: string ) => { setApiKey(newApiKey); setProductId(newProductId); @@ -244,14 +253,20 @@ Thanks!` setAzureConfig(newAzure); setEmailTemplates(newEmailTemplates); - localStorage.setItem('infomaniak_api_key', newApiKey); - localStorage.setItem('infomaniak_product_id', newProductId); - localStorage.setItem('infomaniak_prompts', JSON.stringify(newPrompts)); - localStorage.setItem('infomaniak_save_path', newSavePath); + setDailyBackupEnabled(newDailyBackupEnabled); + setDailyBackupPath(newDailyBackupPath); + + localStorage.setItem('hearbit_api_key', newApiKey); + localStorage.setItem('hearbit_product_id', newProductId); + localStorage.setItem('hearbit_prompts', JSON.stringify(newPrompts)); + localStorage.setItem('hearbit_save_path', newSavePath); localStorage.setItem('hearbit_smtp_config', JSON.stringify(newSmtp)); localStorage.setItem('hearbit_azure_config', JSON.stringify(newAzure)); localStorage.setItem('hearbit_email_templates', JSON.stringify(newEmailTemplates)); + localStorage.setItem('hearbit_daily_backup_enabled', String(newDailyBackupEnabled)); + localStorage.setItem('hearbit_daily_backup_path', newDailyBackupPath); + setView(lastTab); }; @@ -332,6 +347,80 @@ Thanks!` setView('transcription'); // Switch to Transcription view to see content }; + const performBackup = useCallback(async (isAuto = false) => { + try { + if (isAuto && !dailyBackupEnabled) return; + + const dataToBackup = { + apiKey, + productId, + prompts, + savePath, + smtp: smtpConfig, + azure: azureConfig, + emailTemplates, + history, // Including history! + // Also include daily backup settings so they persist on restore + dailyBackup: { + enabled: dailyBackupEnabled, + path: dailyBackupPath, + } + }; + + // Always save as JSON (no encryption) + const content = JSON.stringify(dataToBackup, null, 2); + + const dateStr = new Date().toISOString().slice(0, 10); + const fileName = `hearbit_backup_${isAuto ? 'auto_' : ''}${dateStr}.json`; + + // Determine path: use specific daily backup path, or general savePath + const targetDir = (isAuto ? dailyBackupPath : savePath) || savePath; + + if (!targetDir) { + if (!isAuto) addToast('No backup path configured.', 'error'); + return; + } + + const fullPath = `${targetDir}/${fileName}`; + + await invoke('save_text_file', { path: fullPath, content }); + + if (isAuto) { + const now = new Date().toISOString(); + setLastBackupDate(now); + localStorage.setItem('hearbit_last_backup_date', now); + console.log("Auto-backup completed:", fullPath); + } else { + addToast(`Backup saved to ${fullPath}`, 'success'); + } + + } catch (e) { + console.error("Backup failed:", e); + if (!isAuto) addToast(`Backup failed: ${e}`, 'error'); + } + }, [apiKey, productId, prompts, savePath, smtpConfig, azureConfig, emailTemplates, history, dailyBackupEnabled, dailyBackupPath]); + + // Check for Daily Backup on Mount / State Change + useEffect(() => { + if (!dailyBackupEnabled) return; + + const check = async () => { + const today = new Date().toISOString().slice(0, 10); + const last = lastBackupDate ? lastBackupDate.slice(0, 10) : ''; + + if (last !== today) { + // Perform backup + await performBackup(true); + } + }; + + const timer = setTimeout(() => { + check(); + }, 5000); // Check 5s after load to allow state to settle + + return () => clearTimeout(timer); + }, [dailyBackupEnabled, lastBackupDate, performBackup]); + return ( @@ -474,6 +563,18 @@ Thanks!` smtpConfig={smtpConfig} azureConfig={azureConfig} emailTemplates={emailTemplates} + + // Pass new backup props + dailyBackupEnabled={dailyBackupEnabled} + dailyBackupPath={dailyBackupPath} + lastBackupDate={lastBackupDate} + + // Pass history and update callback + history={history} + onHistoryUpdate={(newHistory) => { + setHistory(newHistory); + localStorage.setItem('infomaniak_history', JSON.stringify(newHistory)); + }} /> )} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index c714aba..fa281b1 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,9 +1,7 @@ import React, { useState, useEffect } from 'react'; -import { Save, FolderOpen, Lock, Upload, Download, Eye, EyeOff, Mail, FileText, ScrollText } from 'lucide-react'; +import { Save, FolderOpen, Lock, Upload, Download, Mail, FileText, ScrollText } from 'lucide-react'; import { save, open } from '@tauri-apps/plugin-dialog'; -// Removed writeTextFile as we use invoke 'save_text_file' import { invoke } from '@tauri-apps/api/core'; -import { encryptData, decryptData } from '../utils/backup'; import EmailTemplateEditor from './EmailTemplateEditor'; import logo from '../assets/logo.png'; @@ -17,6 +15,10 @@ interface SettingsProps { emailTemplates: EmailTemplate[]; smtpConfig: SmtpConfig; azureConfig: AzureConfig; + dailyBackupEnabled: boolean; + dailyBackupPath: string; + lastBackupDate: string; + history: any[]; onSave: ( apiKey: string, productId: string, @@ -24,8 +26,11 @@ interface SettingsProps { savePath: string, smtp: SmtpConfig, azure: AzureConfig, - emailTemplates: EmailTemplate[] + emailTemplates: EmailTemplate[], + dailyBackupEnabled: boolean, + dailyBackupPath: string ) => void; + onHistoryUpdate: (history: any[]) => void; onClose: () => void; } @@ -52,14 +57,10 @@ const Settings: React.FC = ({ apiKey, productId, prompts, savePat const [localEmailTemplates, setLocalEmailTemplates] = useState(props.emailTemplates); // New state const [localSmtp, setLocalSmtp] = useState(props.smtpConfig); const [localAzure, setLocalAzure] = useState(props.azureConfig); + const [localDailyBackupEnabled, setLocalDailyBackupEnabled] = useState(props.dailyBackupEnabled); + const [localDailyBackupPath, setLocalDailyBackupPath] = useState(props.dailyBackupPath); const [statusIdx, setStatusIdx] = useState(null); - // Backup & Restore State - const [backupPassword, setBackupPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [isImportModalOpen, setIsImportModalOpen] = useState(false); - const [importFileContent, setImportFileContent] = useState(null); - // Email Template Editor State const [editingTemplate, setEditingTemplate] = useState(null); const [isEmailEditorOpen, setIsEmailEditorOpen] = useState(false); @@ -133,7 +134,17 @@ const Settings: React.FC = ({ apiKey, productId, prompts, savePat }; const handleSave = () => { - onSave(localApiKey, localProductId, localPrompts, localSavePath, localSmtp, localAzure, localEmailTemplates); + onSave( + localApiKey, + localProductId, + localPrompts, + localSavePath, + localSmtp, + localAzure, + localEmailTemplates, + localDailyBackupEnabled, + localDailyBackupPath + ); onClose(); }; @@ -154,10 +165,6 @@ const Settings: React.FC = ({ apiKey, productId, prompts, savePat }; const handleExport = async () => { - if (!backupPassword) { - setStatusIdx('Error: Password required to encrypt backup.'); - return; - } try { const data = { apiKey: localApiKey, @@ -165,21 +172,28 @@ const Settings: React.FC = ({ apiKey, productId, prompts, savePat prompts: localPrompts, savePath: localSavePath, smtp: localSmtp, - azure: localAzure + azure: localAzure, + emailTemplates: localEmailTemplates, + history: props.history, + dailyBackup: { + enabled: localDailyBackupEnabled, + path: localDailyBackupPath, + } }; - const encrypted = await encryptData(data, backupPassword); + + // Always save as JSON (no encryption) + const content = JSON.stringify(data, null, 2); const filePath = await save({ - defaultPath: `hearbit_backup_${new Date().toISOString().slice(0, 10)}.conf`, + defaultPath: `hearbit_backup_${new Date().toISOString().slice(0, 10)}.json`, filters: [{ name: 'Hearbit Config', - extensions: ['conf'] + extensions: ['json'] }] }); if (filePath) { - // Use backend invoke to write file (bypasses fs scope issues) - await invoke('save_text_file', { path: filePath, content: encrypted }); + await invoke('save_text_file', { path: filePath, content }); setStatusIdx(`Configuration exported to: ${filePath}`); } } catch (e) { @@ -199,41 +213,39 @@ const Settings: React.FC = ({ apiKey, productId, prompts, savePat const reader = new FileReader(); reader.onload = (event) => { if (event.target?.result) { - setImportFileContent(event.target.result as string); - setIsImportModalOpen(true); - setBackupPassword(''); + const content = event.target.result as string; + // Directly import without password modal since we don't use encryption + try { + const data = JSON.parse(content); + + if (data.apiKey) setLocalApiKey(data.apiKey); + if (data.productId) setLocalProductId(data.productId); + if (data.prompts) setLocalPrompts(data.prompts); + if (data.emailTemplates) setLocalEmailTemplates(data.emailTemplates); + if (data.savePath) setLocalSavePath(data.savePath); + if (data.smtp) setLocalSmtp(data.smtp); + if (data.azure) setLocalAzure(data.azure); + if (data.dailyBackup) { + if (data.dailyBackup.enabled !== undefined) setLocalDailyBackupEnabled(data.dailyBackup.enabled); + if (data.dailyBackup.path) setLocalDailyBackupPath(data.dailyBackup.path); + } + // Import history! + if (data.history && Array.isArray(data.history)) { + props.onHistoryUpdate(data.history); + } + + setStatusIdx('Configuration imported! Click Save to apply.'); + } catch (error) { + console.error(error); + setStatusIdx(`Import failed: ${error}`); + } } }; reader.readAsText(file); e.target.value = ''; }; - const confirmImport = async () => { - if (!backupPassword) { - setStatusIdx('Error: Password required to decrypt.'); - return; - } - if (!importFileContent) return; - try { - const data = await decryptData(importFileContent, backupPassword); - if (data.apiKey) setLocalApiKey(data.apiKey); - if (data.productId) setLocalProductId(data.productId); - - if (data.prompts) setLocalPrompts(data.prompts); - if (data.emailTemplates) setLocalEmailTemplates(data.emailTemplates); - if (data.savePath) setLocalSavePath(data.savePath); - if (data.smtp) setLocalSmtp(data.smtp); - if (data.azure) setLocalAzure(data.azure); - - setStatusIdx('Configuration imported! Click Save to apply.'); - setIsImportModalOpen(false); - setImportFileContent(null); - } catch (e) { - console.error(e); - setStatusIdx('Import failed: Wrong password or corrupted file.'); - } - }; const handleCreateDevice = async () => { try { @@ -257,49 +269,6 @@ const Settings: React.FC = ({ apiKey, productId, prompts, savePat return (
{/* Import Password Modal */} - {isImportModalOpen && ( -
-
-
- Import Configuration -
-

- Enter the password used to encrypt this backup file. -

-
- setBackupPassword(e.target.value)} - placeholder="Backup Password" - className="w-full p-2 pr-8 rounded border border-border bg-secondary text-foreground focus:ring-2 focus:ring-primary outline-none" - /> - -
-
- - -
-
-
- )} - {/* Email Template Editor Modal */} = ({ apiKey, productId, prompts, savePat onClick={() => removePrompt(prompt.id)} className="absolute top-2 right-2 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity text-xs flex items-center gap-1" > - Remove + Remove = ({ apiKey, productId, prompts, savePat {activeTab === 'backup' && (
+ {/* Manual Configuration Backup */}
-

Configuration Backup

+

Manual Configuration Backup

- Securely export your settings, including API keys and prompts. You must set a password to encrypt the backup file. + Export all your settings, including API keys, prompts, email templates, and history as JSON files.

-
- - setBackupPassword(e.target.value)} - placeholder="Enter a strong password" - className="w-full p-2 pr-8 rounded border border-border bg-secondary text-foreground focus:ring-2 focus:ring-primary outline-none text-sm" - /> - -
-
+ + {/* Daily Automated Backup */} +
+

Daily Automated Backup

+

+ Automatically backup your configuration once per day to prevent data loss. Backups are saved as JSON files. +

+ +
+ setLocalDailyBackupEnabled(e.target.checked)} + className="w-4 h-4" + /> + +
+ + {localDailyBackupEnabled && ( +
+ +
+ setLocalDailyBackupPath(e.target.value)} + placeholder="Leave empty to use recordings folder" + className="flex-1 p-2 rounded border border-border bg-secondary text-foreground focus:ring-2 focus:ring-primary outline-none text-sm" + /> + +
+

+ Last backup: {props.lastBackupDate || 'Never'} +

+
+ )} +
)}