From f7e8ccb5b63e0c31b30e08b71d417e5f77d92fd8 Mon Sep 17 00:00:00 2001 From: Kenearos Date: Tue, 12 May 2026 00:13:20 +0200 Subject: [PATCH] feat(storage): add getApiKey/setApiKey/clearApiKey/getApiModel/setApiModel for OpenRouter integration --- storage.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ test-suite.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/storage.js b/storage.js index 3df3678..9cd8c8d 100644 --- a/storage.js +++ b/storage.js @@ -6,6 +6,9 @@ class DataStorage { constructor() { this.STORAGE_KEY_EMPLOYEES = 'dienstplan_employees'; this.STORAGE_KEY_DUTIES = 'dienstplan_duties'; + this.STORAGE_KEY_OPENROUTER_KEY = 'dienstplan_openrouter_key'; + this.STORAGE_KEY_OPENROUTER_MODEL = 'dienstplan_openrouter_model'; + this.DEFAULT_MODEL = 'anthropic/claude-sonnet-4.6'; } /** @@ -307,6 +310,67 @@ class DataStorage { return false; } } + + // ============================================================ + // API Key / Model (Feature A: Bild-Import) + // Device-local config — NOT included in exportData/clearAll. + // ============================================================ + + /** + * @returns {string|null} + */ + getApiKey() { + try { + return localStorage.getItem(this.STORAGE_KEY_OPENROUTER_KEY) || null; + } catch (e) { + console.error('Fehler beim Laden des API-Keys:', e); + return null; + } + } + + /** + * @param {string} key + */ + setApiKey(key) { + try { + localStorage.setItem(this.STORAGE_KEY_OPENROUTER_KEY, String(key)); + } catch (e) { + console.error('Fehler beim Speichern des API-Keys:', e); + throw e; + } + } + + clearApiKey() { + try { + localStorage.removeItem(this.STORAGE_KEY_OPENROUTER_KEY); + } catch (e) { + console.error('Fehler beim Loeschen des API-Keys:', e); + } + } + + /** + * @returns {string} + */ + getApiModel() { + try { + return localStorage.getItem(this.STORAGE_KEY_OPENROUTER_MODEL) || this.DEFAULT_MODEL; + } catch (e) { + console.error('Fehler beim Laden des Modells:', e); + return this.DEFAULT_MODEL; + } + } + + /** + * @param {string} modelId + */ + setApiModel(modelId) { + try { + localStorage.setItem(this.STORAGE_KEY_OPENROUTER_MODEL, String(modelId)); + } catch (e) { + console.error('Fehler beim Speichern des Modells:', e); + throw e; + } + } } // Make it available globally diff --git a/test-suite.js b/test-suite.js index 420306e..8b8f4cb 100644 --- a/test-suite.js +++ b/test-suite.js @@ -473,6 +473,60 @@ runner.test('Edge Case: Dienst am 29. Februar (Schaltjahr)', (t) => { t.assertEqual(result.normalDays, 1.0, 'Sollte normalen Tag erkennen'); }); +// ============================================================================ +// Storage Tests - API Key / Model (Feature A) +// ============================================================================ + +runner.test('Storage API Key: setApiKey/getApiKey round-trip', (t) => { + const storage = new DataStorage(); + storage.clearApiKey(); + storage.setApiKey('sk-or-test-12345'); + t.assertEqual(storage.getApiKey(), 'sk-or-test-12345', 'Key sollte gespeichert sein'); + storage.clearApiKey(); +}); + +runner.test('Storage API Key: getApiKey ohne gesetzten Wert liefert null', (t) => { + const storage = new DataStorage(); + storage.clearApiKey(); + t.assertEqual(storage.getApiKey(), null, 'Sollte null sein'); +}); + +runner.test('Storage API Key: clearApiKey entfernt den Key', (t) => { + const storage = new DataStorage(); + storage.setApiKey('sk-or-test'); + storage.clearApiKey(); + t.assertEqual(storage.getApiKey(), null, 'Key sollte gelöscht sein'); +}); + +runner.test('Storage API Model: Default ist anthropic/claude-sonnet-4.6', (t) => { + const storage = new DataStorage(); + localStorage.removeItem('dienstplan_openrouter_model'); + t.assertEqual(storage.getApiModel(), 'anthropic/claude-sonnet-4.6', 'Default-Modell'); +}); + +runner.test('Storage API Model: setApiModel/getApiModel round-trip', (t) => { + const storage = new DataStorage(); + storage.setApiModel('google/gemini-2.5-pro'); + t.assertEqual(storage.getApiModel(), 'google/gemini-2.5-pro', 'Modell sollte gespeichert sein'); + localStorage.removeItem('dienstplan_openrouter_model'); +}); + +runner.test('Storage API Key: exportData enthält keinen API-Key', (t) => { + const storage = new DataStorage(); + storage.setApiKey('sk-or-secret'); + const exported = storage.exportData(); + t.assertFalse(exported.includes('sk-or-secret'), 'Key darf nicht im Export sein'); + storage.clearApiKey(); +}); + +runner.test('Storage API Key: clearAll laesst API-Key unberuehrt', (t) => { + const storage = new DataStorage(); + storage.setApiKey('sk-or-keep'); + storage.clearAll(); + t.assertEqual(storage.getApiKey(), 'sk-or-keep', 'Key sollte clearAll ueberleben'); + storage.clearApiKey(); +}); + // ============================================================================ // Display Functions // ============================================================================ @@ -505,6 +559,8 @@ async function runAllTests() { 'Calculator - Tag-Klassifizierung': [], 'Calculator - Bonusberechnung': [], 'Storage': [], + 'Storage API Key': [], + 'Image Importer': [], 'Edge Cases': [] }; @@ -515,8 +571,12 @@ async function runAllTests() { categories['Calculator - Tag-Klassifizierung'].push(result); } else if (result.name.includes('Berechnung:')) { categories['Calculator - Bonusberechnung'].push(result); + } else if (result.name.startsWith('Storage API')) { + categories['Storage API Key'].push(result); } else if (result.name.includes('Storage:')) { categories['Storage'].push(result); + } else if (result.name.startsWith('ImageImporter') || result.name.startsWith('Levenshtein') || result.name.startsWith('Parse') || result.name.startsWith('Match') || result.name.startsWith('Preprocess') || result.name.startsWith('Resolve') || result.name.startsWith('CallVision') || result.name.startsWith('Classify')) { + categories['Image Importer'].push(result); } else if (result.name.includes('Edge Case:')) { categories['Edge Cases'].push(result); }