Release 1.2.0: Remove backup encryption and switch to JSON-only backups with history support
This commit is contained in:
113
src/App.tsx
113
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));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
||||
const [localEmailTemplates, setLocalEmailTemplates] = useState<EmailTemplate[]>(props.emailTemplates); // New state
|
||||
const [localSmtp, setLocalSmtp] = useState<SmtpConfig>(props.smtpConfig);
|
||||
const [localAzure, setLocalAzure] = useState<AzureConfig>(props.azureConfig);
|
||||
const [localDailyBackupEnabled, setLocalDailyBackupEnabled] = useState(props.dailyBackupEnabled);
|
||||
const [localDailyBackupPath, setLocalDailyBackupPath] = useState(props.dailyBackupPath);
|
||||
const [statusIdx, setStatusIdx] = useState<string | null>(null);
|
||||
|
||||
// Backup & Restore State
|
||||
const [backupPassword, setBackupPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [importFileContent, setImportFileContent] = useState<string | null>(null);
|
||||
|
||||
// Email Template Editor State
|
||||
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
|
||||
const [isEmailEditorOpen, setIsEmailEditorOpen] = useState(false);
|
||||
@@ -133,7 +134,17 @@ const Settings: React.FC<SettingsProps> = ({ 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<SettingsProps> = ({ 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<SettingsProps> = ({ 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<SettingsProps> = ({ 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<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-background font-mono text-sm relative">
|
||||
{/* Import Password Modal */}
|
||||
{isImportModalOpen && (
|
||||
<div className="absolute inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
|
||||
<div className="bg-background border border-border rounded-lg shadow-xl p-6 w-full max-w-sm space-y-4">
|
||||
<div className="flex items-center gap-2 text-foreground font-semibold">
|
||||
<Lock size={16} /> Import Configuration
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Enter the password used to encrypt this backup file.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={backupPassword}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-2 top-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
onClick={() => setIsImportModalOpen(false)}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded border border-border hover:bg-secondary text-foreground transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmImport}
|
||||
disabled={!backupPassword}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Decrypt & Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Template Editor Modal */}
|
||||
<EmailTemplateEditor
|
||||
isOpen={isEmailEditorOpen}
|
||||
@@ -471,7 +440,7 @@ const Settings: React.FC<SettingsProps> = ({ 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"
|
||||
>
|
||||
<EyeOff size={14} /> Remove
|
||||
Remove
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
@@ -619,31 +588,13 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
||||
|
||||
{activeTab === 'backup' && (
|
||||
<div className="space-y-6 max-w-xl">
|
||||
{/* Manual Configuration Backup */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-foreground font-semibold border-b border-border pb-2">Configuration Backup</h3>
|
||||
<h3 className="text-foreground font-semibold border-b border-border pb-2">Manual Configuration Backup</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="relative">
|
||||
<label className="block text-xs font-semibold text-muted-foreground mb-1 uppercase tracking-wide">
|
||||
Encryption Password
|
||||
</label>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={backupPassword}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-2 top-8 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-2">
|
||||
<button
|
||||
onClick={handleExport}
|
||||
@@ -660,12 +611,68 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
||||
<input
|
||||
type="file"
|
||||
id="import-file-input"
|
||||
accept=".conf"
|
||||
accept=".json"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Daily Automated Backup */}
|
||||
<div className="space-y-4 border-t border-border pt-6">
|
||||
<h3 className="text-foreground font-semibold border-b border-border pb-2">Daily Automated Backup</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Automatically backup your configuration once per day to prevent data loss. Backups are saved as JSON files.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable-daily-backup"
|
||||
checked={localDailyBackupEnabled}
|
||||
onChange={(e) => setLocalDailyBackupEnabled(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label htmlFor="enable-daily-backup" className="text-sm text-foreground cursor-pointer">
|
||||
Enable daily automated backup
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{localDailyBackupEnabled && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-muted-foreground mb-1 uppercase tracking-wide">
|
||||
Backup Location
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={localDailyBackupPath}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const selected = await open({ directory: true, multiple: false });
|
||||
if (selected && typeof selected === 'string') {
|
||||
setLocalDailyBackupPath(selected);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
className="p-2 aspect-square flex items-center justify-center bg-secondary hover:bg-secondary/80 border border-border rounded text-foreground transition-all active:scale-95"
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
Last backup: {props.lastBackupDate || 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user