diff --git a/src/lib/components/panels/GuardianPanel.svelte b/src/lib/components/panels/GuardianPanel.svelte index a894ea1..ea601b2 100644 --- a/src/lib/components/panels/GuardianPanel.svelte +++ b/src/lib/components/panels/GuardianPanel.svelte @@ -1,6 +1,7 @@ + +
+ +
+

+ + Export Data +

+ +
+ +
+
+
+

Complete Backup

+

+ Export all pets, settings, and journal entries +

+
+ +
+
+ + + {#if pets.length > 0} +
+

Export Individual Pets

+
+ {#each pets as pet} +
+
+
+ {pet.name} { + const target = e.target as HTMLImageElement; + target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iMTYiIGZpbGw9IiNGMzRGNEYiLz4KPHN2ZyB4PSI4IiB5PSI4IiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9IndoaXRlIj4KICA8cGF0aCBkPSJNNS4yNSA0QzQuNTU5NjQgNCA0IDQuNTU5NjQgNCA1LjI1VjEwLjc1QzQgMTEuNDQwNCA0LjU1OTY0IDEyIDUuMjUgMTJIMTAuNzVDMTEuNDQwNCAxMiAxMiAxMS40NDA0IDEyIDEwLjc1VjUuMjVDMTIgNC41NTk2NCAxMS40NDA0IDQgMTAuNzUgNEg1LjI1WiIvPgo8L3N2Zz4KPC9zdmc+'; + }} + /> +
+

{pet.name}

+

+ {pet.journalEntries?.length || 0} entries +

+
+
+ +
+
+ {/each} +
+
+ {/if} +
+
+ + +
+

+ + Import Data +

+ +
+ +

+ Select a JSONL backup file to import +

+ + + + {#if importMessage} +
+

+ {importMessage} +

+
+ {/if} +
+ + +
+

Data Format

+
    +
  • • JSONL files exported from Petalytics
  • +
  • • Individual pet files or complete backups
  • +
  • • All journal entries and AI analyses included
  • +
  • • Import will merge with existing data
  • +
+
+
+
\ No newline at end of file diff --git a/src/lib/stores/guardian.ts b/src/lib/stores/guardian.ts index f1b39bf..139ba25 100644 --- a/src/lib/stores/guardian.ts +++ b/src/lib/stores/guardian.ts @@ -80,6 +80,15 @@ export const guardianHelpers = { localStorage.removeItem(STORAGE_KEY); } guardianStore.set(defaultGuardian); + }, + + // Import guardian data + importGuardian(guardianData: any) { + guardianStore.update(current => { + const updated = { ...current, ...guardianData }; + this.save(updated); + return updated; + }); } }; diff --git a/src/lib/stores/pets.ts b/src/lib/stores/pets.ts index 2ee215b..158f3bb 100644 --- a/src/lib/stores/pets.ts +++ b/src/lib/stores/pets.ts @@ -101,6 +101,47 @@ export const petHelpers = { } return pet; }); + this.save(updated); + return updated; + }); + }, + + // Get all pets + getAll(callback?: (pets: PetPanelData[]) => void): (() => void) | undefined { + if (callback) { + return petStore.subscribe(callback); + } else { + let pets: PetPanelData[] = []; + const unsubscribe = petStore.subscribe(value => { pets = value; }); + unsubscribe(); + return undefined; + } + }, + + // Get all pets synchronously + getAllPets(): PetPanelData[] { + let pets: PetPanelData[] = []; + const unsubscribe = petStore.subscribe(value => { pets = value; }); + unsubscribe(); + return pets; + }, + + // Import pet data + importPet(petData: PetPanelData) { + petStore.update(pets => { + const existingIndex = pets.findIndex(p => p.id === petData.id); + let updated: PetPanelData[]; + + if (existingIndex >= 0) { + // Update existing pet + updated = pets.map((pet, index) => + index === existingIndex ? petData : pet + ); + } else { + // Add new pet + updated = [...pets, petData]; + } + this.save(updated); return updated; }); diff --git a/src/lib/utils/data-export.ts b/src/lib/utils/data-export.ts new file mode 100644 index 0000000..e7ce000 --- /dev/null +++ b/src/lib/utils/data-export.ts @@ -0,0 +1,216 @@ +import type { PetPanelData } from '$lib/types/Pet'; +import type { Guardian } from '$lib/types/Guardian'; +import { petHelpers } from '$lib/stores/pets'; +import { guardianHelpers } from '$lib/stores/guardian'; +import { aiAnalysisHelpers } from '$lib/stores/ai-analysis'; + +export interface PetDataExport { + version: string; + exportDate: string; + pet: PetPanelData; + aiAnalyses: Record; +} + +export interface FullDataExport { + version: string; + exportDate: string; + guardian: any; + pets: PetPanelData[]; + aiAnalyses: Record; +} + +export class DataExporter { + private static readonly CURRENT_VERSION = '1.0.0'; + + // Export single pet as JSONL file + static async exportPet(pet: PetPanelData): Promise { + try { + const exportData: PetDataExport = { + version: this.CURRENT_VERSION, + exportDate: new Date().toISOString(), + pet: pet, + aiAnalyses: this.getAnalysesForPet(pet.id) + }; + + const jsonlContent = Object.keys(exportData).map(key => + JSON.stringify({ [key]: exportData[key as keyof PetDataExport] }) + ).join('\n'); + + this.downloadFile(jsonlContent, `${pet.name.toLowerCase().replace(/\s+/g, '_')}.jsonl`); + } catch (error) { + throw new Error('Failed to export pet data'); + } + } + + // Export all data + static async exportAllData(): Promise { + try { + const pets = this.getAllPets(); + const guardian = this.getGuardian(); + + const exportData: FullDataExport = { + version: this.CURRENT_VERSION, + exportDate: new Date().toISOString(), + guardian: guardian, + pets: pets, + aiAnalyses: this.getAllAnalyses() + }; + + const jsonlContent = Object.keys(exportData).map(key => + JSON.stringify({ [key]: exportData[key as keyof FullDataExport] }) + ).join('\n'); + + this.downloadFile(jsonlContent, `petalytics_backup_${this.formatDate(new Date())}.jsonl`); + } catch (error) { + throw new Error('Failed to export all data'); + } + } + + // Import JSONL file + static async importFromFile(file: File): Promise<{ success: boolean; message: string }> { + try { + if (!file.name.endsWith('.jsonl')) { + return { success: false, message: 'Please select a valid JSONL file' }; + } + + const content = await this.readFileAsText(file); + const importData = this.parseJSONL(content); + + const validation = this.validateImportData(importData); + if (!validation.valid) { + return { success: false, message: validation.error || 'Invalid data format' }; + } + + if (importData.pets) { + return await this.importFullBackup(importData); + } else if (importData.pet) { + return await this.importSinglePet(importData); + } + + return { success: false, message: 'Invalid data format' }; + } catch (error) { + return { success: false, message: 'Failed to import data: ' + (error as Error).message }; + } + } + + private static parseJSONL(content: string): any { + const lines = content.trim().split('\n'); + let result: any = {}; + + for (const line of lines) { + if (line.trim()) { + const parsed = JSON.parse(line); + result = { ...result, ...parsed }; + } + } + + return result; + } + + private static downloadFile(content: string, filename: string): void { + const blob = new Blob([content], { type: 'application/jsonl' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); + } + + private static readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + }); + } + + private static formatDate(date: Date): string { + return date.toISOString().split('T')[0]; + } + + private static getAllPets(): PetPanelData[] { + return petHelpers.getAllPets(); + } + + private static getGuardian(): any { + return guardianHelpers.load(); + } + + private static getAnalysesForPet(petId: string): Record { + // Get AI analyses for specific pet from journal entries + const analyses: Record = {}; + const pet = petHelpers.getPet(petId); + if (pet?.journalEntries) { + pet.journalEntries.forEach(entry => { + const analysis = aiAnalysisHelpers.getAnalysis(entry.id); + if (analysis) { + analyses[entry.id] = analysis; + } + }); + } + return analyses; + } + + private static getAllAnalyses(): Record { + // Get all AI analyses from all pets + const allAnalyses: Record = {}; + const pets = this.getAllPets(); + pets.forEach(pet => { + const petAnalyses = this.getAnalysesForPet(pet.id); + Object.assign(allAnalyses, petAnalyses); + }); + return allAnalyses; + } + + private static validateImportData(data: any): { valid: boolean; error?: string } { + if (!data.version || !data.exportDate) { + return { valid: false, error: 'Invalid file format - missing version or export date' }; + } + + // Check version compatibility + const [majorVersion] = data.version.split('.'); + const [currentMajor] = this.CURRENT_VERSION.split('.'); + if (majorVersion !== currentMajor) { + return { valid: false, error: 'Incompatible file version' }; + } + + // Validate structure + if (data.pets && !Array.isArray(data.pets)) { + return { valid: false, error: 'Invalid pets data format' }; + } + + if (data.pet && (!data.pet.id || !data.pet.name)) { + return { valid: false, error: 'Invalid pet data format' }; + } + + return { valid: true }; + } + + private static async importFullBackup(data: FullDataExport): Promise<{ success: boolean; message: string }> { + let count = 0; + + if (data.guardian) { + guardianHelpers.importGuardian(data.guardian); + } + + if (data.pets?.length) { + for (const pet of data.pets) { + petHelpers.importPet(pet); + count++; + } + } + + return { success: true, message: `Imported ${count} pets successfully` }; + } + + private static async importSinglePet(data: PetDataExport): Promise<{ success: boolean; message: string }> { + petHelpers.importPet(data.pet); + return { success: true, message: `Imported ${data.pet.name} successfully` }; + } +} \ No newline at end of file