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 as SettingsIcon } from "lucide-react";
|
||||||
import Settings, { SmtpConfig, AzureConfig } from "./components/Settings";
|
import Settings, { SmtpConfig, AzureConfig } from "./components/Settings";
|
||||||
import Recorder from "./components/Recorder";
|
import Recorder from "./components/Recorder";
|
||||||
@@ -60,6 +61,11 @@ function App() {
|
|||||||
return localStorage.getItem('hearbit_selected_model') || 'mixtral';
|
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) => {
|
const handleModelChange = (model: string) => {
|
||||||
setSelectedModel(model);
|
setSelectedModel(model);
|
||||||
localStorage.setItem('hearbit_selected_model', model);
|
localStorage.setItem('hearbit_selected_model', model);
|
||||||
@@ -227,6 +233,7 @@ Thanks!`
|
|||||||
return saved ? JSON.parse(saved) : defaultEmailTemplates;
|
return saved ? JSON.parse(saved) : defaultEmailTemplates;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleSaveSettings = (
|
const handleSaveSettings = (
|
||||||
newApiKey: string,
|
newApiKey: string,
|
||||||
newProductId: string,
|
newProductId: string,
|
||||||
@@ -234,7 +241,9 @@ Thanks!`
|
|||||||
newSavePath: string,
|
newSavePath: string,
|
||||||
newSmtp: SmtpConfig,
|
newSmtp: SmtpConfig,
|
||||||
newAzure: AzureConfig,
|
newAzure: AzureConfig,
|
||||||
newEmailTemplates: EmailTemplate[]
|
newEmailTemplates: EmailTemplate[],
|
||||||
|
newDailyBackupEnabled: boolean,
|
||||||
|
newDailyBackupPath: string
|
||||||
) => {
|
) => {
|
||||||
setApiKey(newApiKey);
|
setApiKey(newApiKey);
|
||||||
setProductId(newProductId);
|
setProductId(newProductId);
|
||||||
@@ -244,14 +253,20 @@ Thanks!`
|
|||||||
setAzureConfig(newAzure);
|
setAzureConfig(newAzure);
|
||||||
setEmailTemplates(newEmailTemplates);
|
setEmailTemplates(newEmailTemplates);
|
||||||
|
|
||||||
localStorage.setItem('infomaniak_api_key', newApiKey);
|
setDailyBackupEnabled(newDailyBackupEnabled);
|
||||||
localStorage.setItem('infomaniak_product_id', newProductId);
|
setDailyBackupPath(newDailyBackupPath);
|
||||||
localStorage.setItem('infomaniak_prompts', JSON.stringify(newPrompts));
|
|
||||||
localStorage.setItem('infomaniak_save_path', newSavePath);
|
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_smtp_config', JSON.stringify(newSmtp));
|
||||||
localStorage.setItem('hearbit_azure_config', JSON.stringify(newAzure));
|
localStorage.setItem('hearbit_azure_config', JSON.stringify(newAzure));
|
||||||
localStorage.setItem('hearbit_email_templates', JSON.stringify(newEmailTemplates));
|
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);
|
setView(lastTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -332,6 +347,80 @@ Thanks!`
|
|||||||
setView('transcription'); // Switch to Transcription view to see content
|
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 (
|
return (
|
||||||
@@ -474,6 +563,18 @@ Thanks!`
|
|||||||
smtpConfig={smtpConfig}
|
smtpConfig={smtpConfig}
|
||||||
azureConfig={azureConfig}
|
azureConfig={azureConfig}
|
||||||
emailTemplates={emailTemplates}
|
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>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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';
|
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 { invoke } from '@tauri-apps/api/core';
|
||||||
import { encryptData, decryptData } from '../utils/backup';
|
|
||||||
import EmailTemplateEditor from './EmailTemplateEditor';
|
import EmailTemplateEditor from './EmailTemplateEditor';
|
||||||
import logo from '../assets/logo.png';
|
import logo from '../assets/logo.png';
|
||||||
|
|
||||||
@@ -17,6 +15,10 @@ interface SettingsProps {
|
|||||||
emailTemplates: EmailTemplate[];
|
emailTemplates: EmailTemplate[];
|
||||||
smtpConfig: SmtpConfig;
|
smtpConfig: SmtpConfig;
|
||||||
azureConfig: AzureConfig;
|
azureConfig: AzureConfig;
|
||||||
|
dailyBackupEnabled: boolean;
|
||||||
|
dailyBackupPath: string;
|
||||||
|
lastBackupDate: string;
|
||||||
|
history: any[];
|
||||||
onSave: (
|
onSave: (
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
productId: string,
|
productId: string,
|
||||||
@@ -24,8 +26,11 @@ interface SettingsProps {
|
|||||||
savePath: string,
|
savePath: string,
|
||||||
smtp: SmtpConfig,
|
smtp: SmtpConfig,
|
||||||
azure: AzureConfig,
|
azure: AzureConfig,
|
||||||
emailTemplates: EmailTemplate[]
|
emailTemplates: EmailTemplate[],
|
||||||
|
dailyBackupEnabled: boolean,
|
||||||
|
dailyBackupPath: string
|
||||||
) => void;
|
) => void;
|
||||||
|
onHistoryUpdate: (history: any[]) => void;
|
||||||
onClose: () => 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 [localEmailTemplates, setLocalEmailTemplates] = useState<EmailTemplate[]>(props.emailTemplates); // New state
|
||||||
const [localSmtp, setLocalSmtp] = useState<SmtpConfig>(props.smtpConfig);
|
const [localSmtp, setLocalSmtp] = useState<SmtpConfig>(props.smtpConfig);
|
||||||
const [localAzure, setLocalAzure] = useState<AzureConfig>(props.azureConfig);
|
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);
|
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
|
// Email Template Editor State
|
||||||
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
|
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
|
||||||
const [isEmailEditorOpen, setIsEmailEditorOpen] = useState(false);
|
const [isEmailEditorOpen, setIsEmailEditorOpen] = useState(false);
|
||||||
@@ -133,7 +134,17 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onSave(localApiKey, localProductId, localPrompts, localSavePath, localSmtp, localAzure, localEmailTemplates);
|
onSave(
|
||||||
|
localApiKey,
|
||||||
|
localProductId,
|
||||||
|
localPrompts,
|
||||||
|
localSavePath,
|
||||||
|
localSmtp,
|
||||||
|
localAzure,
|
||||||
|
localEmailTemplates,
|
||||||
|
localDailyBackupEnabled,
|
||||||
|
localDailyBackupPath
|
||||||
|
);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,10 +165,6 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
if (!backupPassword) {
|
|
||||||
setStatusIdx('Error: Password required to encrypt backup.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
apiKey: localApiKey,
|
apiKey: localApiKey,
|
||||||
@@ -165,21 +172,28 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
prompts: localPrompts,
|
prompts: localPrompts,
|
||||||
savePath: localSavePath,
|
savePath: localSavePath,
|
||||||
smtp: localSmtp,
|
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({
|
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: [{
|
filters: [{
|
||||||
name: 'Hearbit Config',
|
name: 'Hearbit Config',
|
||||||
extensions: ['conf']
|
extensions: ['json']
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
// Use backend invoke to write file (bypasses fs scope issues)
|
await invoke('save_text_file', { path: filePath, content });
|
||||||
await invoke('save_text_file', { path: filePath, content: encrypted });
|
|
||||||
setStatusIdx(`Configuration exported to: ${filePath}`);
|
setStatusIdx(`Configuration exported to: ${filePath}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -199,41 +213,39 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
if (event.target?.result) {
|
if (event.target?.result) {
|
||||||
setImportFileContent(event.target.result as string);
|
const content = event.target.result as string;
|
||||||
setIsImportModalOpen(true);
|
// Directly import without password modal since we don't use encryption
|
||||||
setBackupPassword('');
|
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);
|
reader.readAsText(file);
|
||||||
e.target.value = '';
|
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 () => {
|
const handleCreateDevice = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -257,49 +269,6 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-background font-mono text-sm relative">
|
<div className="flex flex-col h-full bg-background font-mono text-sm relative">
|
||||||
{/* Import Password Modal */}
|
{/* 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 */}
|
{/* Email Template Editor Modal */}
|
||||||
<EmailTemplateEditor
|
<EmailTemplateEditor
|
||||||
isOpen={isEmailEditorOpen}
|
isOpen={isEmailEditorOpen}
|
||||||
@@ -471,7 +440,7 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
onClick={() => removePrompt(prompt.id)}
|
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"
|
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>
|
</button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -619,31 +588,13 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
|
|
||||||
{activeTab === 'backup' && (
|
{activeTab === 'backup' && (
|
||||||
<div className="space-y-6 max-w-xl">
|
<div className="space-y-6 max-w-xl">
|
||||||
|
{/* Manual Configuration Backup */}
|
||||||
<div className="space-y-4">
|
<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">
|
<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>
|
</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">
|
<div className="flex gap-4 pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
@@ -660,12 +611,68 @@ const Settings: React.FC<SettingsProps> = ({ apiKey, productId, prompts, savePat
|
|||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="import-file-input"
|
id="import-file-input"
|
||||||
accept=".conf"
|
accept=".json"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user