keychain permission + modelStateService rely only on db
This commit is contained in:
		
							parent
							
								
									a27ab05fa8
								
							
						
					
					
						commit
						dad74875a0
					
				@ -109,15 +109,15 @@ module.exports = {
 | 
			
		||||
 | 
			
		||||
    // ModelStateService
 | 
			
		||||
    ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
 | 
			
		||||
    ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys());
 | 
			
		||||
    ipcMain.handle('model:get-all-keys', async () => await modelStateService.getAllApiKeys());
 | 
			
		||||
    ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
 | 
			
		||||
    ipcMain.handle('model:remove-api-key', async (e, provider) => await modelStateService.handleRemoveApiKey(provider));
 | 
			
		||||
    ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels());
 | 
			
		||||
    ipcMain.handle('model:get-selected-models', async () => await modelStateService.getSelectedModels());
 | 
			
		||||
    ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId));
 | 
			
		||||
    ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
 | 
			
		||||
    ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
 | 
			
		||||
    ipcMain.handle('model:get-available-models', async (e, { type }) => await modelStateService.getAvailableModels(type));
 | 
			
		||||
    ipcMain.handle('model:are-providers-configured', async () => await modelStateService.areProvidersConfigured());
 | 
			
		||||
    ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
 | 
			
		||||
    ipcMain.handle('model:re-initialize-state', () => modelStateService.initialize());
 | 
			
		||||
    ipcMain.handle('model:re-initialize-state', async () => await modelStateService.initialize());
 | 
			
		||||
 | 
			
		||||
    // LocalAIManager 이벤트를 모든 윈도우에 브로드캐스트
 | 
			
		||||
    localAIManager.on('install-progress', (service, data) => {
 | 
			
		||||
 | 
			
		||||
@ -243,7 +243,7 @@ class AskService {
 | 
			
		||||
            await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
 | 
			
		||||
            console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
 | 
			
		||||
            
 | 
			
		||||
            const modelInfo = modelStateService.getCurrentModelInfo('llm');
 | 
			
		||||
            const modelInfo = await modelStateService.getCurrentModelInfo('llm');
 | 
			
		||||
            if (!modelInfo || !modelInfo.apiKey) {
 | 
			
		||||
                throw new Error('AI model or API key not configured.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -91,7 +91,6 @@ const LATEST_SCHEMA = {
 | 
			
		||||
    },
 | 
			
		||||
    provider_settings: {
 | 
			
		||||
        columns: [
 | 
			
		||||
            { name: 'uid', type: 'TEXT NOT NULL' },
 | 
			
		||||
            { name: 'provider', type: 'TEXT NOT NULL' },
 | 
			
		||||
            { name: 'api_key', type: 'TEXT' },
 | 
			
		||||
            { name: 'selected_llm_model', type: 'TEXT' },
 | 
			
		||||
@ -101,7 +100,7 @@ const LATEST_SCHEMA = {
 | 
			
		||||
            { name: 'created_at', type: 'INTEGER' },
 | 
			
		||||
            { name: 'updated_at', type: 'INTEGER' }
 | 
			
		||||
        ],
 | 
			
		||||
        constraints: ['PRIMARY KEY (uid, provider)']
 | 
			
		||||
        constraints: ['PRIMARY KEY (provider)']
 | 
			
		||||
    },
 | 
			
		||||
    shortcuts: {
 | 
			
		||||
        columns: [
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,13 @@ function createEncryptedConverter(fieldsToEncrypt = []) {
 | 
			
		||||
 | 
			
		||||
            for (const field of fieldsToEncrypt) {
 | 
			
		||||
                 if (Object.prototype.hasOwnProperty.call(appObject, field) && appObject[field] != null) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        appObject[field] = encryptionService.decrypt(appObject[field]);
 | 
			
		||||
                    } catch (error) {
 | 
			
		||||
                        console.warn(`[FirestoreConverter] Failed to decrypt field '${field}' (possibly plaintext or key mismatch):`, error.message);
 | 
			
		||||
                        // Keep the original value instead of failing
 | 
			
		||||
                        // appObject[field] remains as is
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,7 @@
 | 
			
		||||
const sqliteRepository = require('./sqlite.repository');
 | 
			
		||||
 | 
			
		||||
let authService = null;
 | 
			
		||||
 | 
			
		||||
function setAuthService(service) {
 | 
			
		||||
    authService = service;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getBaseRepository() {
 | 
			
		||||
    // For now, we only have sqlite. This could be expanded later.
 | 
			
		||||
    return sqliteRepository;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,71 +9,60 @@ const providerSettingsRepositoryAdapter = {
 | 
			
		||||
    // Core CRUD operations
 | 
			
		||||
    async getByProvider(provider) {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.getByProvider(uid, provider);
 | 
			
		||||
        return await repo.getByProvider(provider);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async getAllByUid() {
 | 
			
		||||
    async getAll() {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.getAllByUid(uid);
 | 
			
		||||
        return await repo.getAll();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async upsert(provider, settings) {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        const now = Date.now();
 | 
			
		||||
        
 | 
			
		||||
        const settingsWithMeta = {
 | 
			
		||||
            ...settings,
 | 
			
		||||
            uid,
 | 
			
		||||
            provider,
 | 
			
		||||
            updated_at: now,
 | 
			
		||||
            created_at: settings.created_at || now
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        return await repo.upsert(uid, provider, settingsWithMeta);
 | 
			
		||||
        return await repo.upsert(provider, settingsWithMeta);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async remove(provider) {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.remove(uid, provider);
 | 
			
		||||
        return await repo.remove(provider);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async removeAllByUid() {
 | 
			
		||||
    async removeAll() {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.removeAllByUid(uid);
 | 
			
		||||
        return await repo.removeAll();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async getRawApiKeysByUid() {
 | 
			
		||||
    async getRawApiKeys() {
 | 
			
		||||
        // This function should always target the local sqlite DB,
 | 
			
		||||
        // as it's part of the local-first boot sequence.
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await sqliteRepository.getRawApiKeysByUid(uid);
 | 
			
		||||
        return await sqliteRepository.getRawApiKeys();
 | 
			
		||||
    },
 | 
			
		||||
    
 | 
			
		||||
    async getActiveProvider(type) {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.getActiveProvider(uid, type);
 | 
			
		||||
        return await repo.getActiveProvider(type);
 | 
			
		||||
    },
 | 
			
		||||
    
 | 
			
		||||
    async setActiveProvider(provider, type) {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.setActiveProvider(uid, provider, type);
 | 
			
		||||
        return await repo.setActiveProvider(provider, type);
 | 
			
		||||
    },
 | 
			
		||||
    
 | 
			
		||||
    async getActiveSettings() {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.getActiveSettings(uid);
 | 
			
		||||
        return await repo.getActiveSettings();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    ...providerSettingsRepositoryAdapter,
 | 
			
		||||
    setAuthService
 | 
			
		||||
    ...providerSettingsRepositoryAdapter
 | 
			
		||||
}; 
 | 
			
		||||
@ -1,31 +1,32 @@
 | 
			
		||||
const sqliteClient = require('../../services/sqliteClient');
 | 
			
		||||
const encryptionService = require('../../services/encryptionService');
 | 
			
		||||
 | 
			
		||||
function getByProvider(uid, provider) {
 | 
			
		||||
function getByProvider(provider) {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? AND provider = ?');
 | 
			
		||||
    const result = stmt.get(uid, provider) || null;
 | 
			
		||||
    const stmt = db.prepare('SELECT * FROM provider_settings WHERE provider = ?');
 | 
			
		||||
    const result = stmt.get(provider) || null;
 | 
			
		||||
    
 | 
			
		||||
    if (result && result.api_key) {
 | 
			
		||||
    if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
			
		||||
        result.api_key = encryptionService.decrypt(result.api_key);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAllByUid(uid) {
 | 
			
		||||
function getAll() {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
 | 
			
		||||
    const results = stmt.all(uid);
 | 
			
		||||
    const stmt = db.prepare('SELECT * FROM provider_settings ORDER BY provider');
 | 
			
		||||
    const results = stmt.all();
 | 
			
		||||
    
 | 
			
		||||
    return results.map(result => {
 | 
			
		||||
        if (result.api_key) {
 | 
			
		||||
            result.api_key = result.api_key;
 | 
			
		||||
        if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
			
		||||
            result.api_key = encryptionService.decrypt(result.api_key);
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function upsert(uid, provider, settings) {
 | 
			
		||||
function upsert(provider, settings) {
 | 
			
		||||
    // Validate: prevent direct setting of active status
 | 
			
		||||
    if (settings.is_active_llm || settings.is_active_stt) {
 | 
			
		||||
        console.warn('[ProviderSettings] Warning: is_active_llm/is_active_stt should not be set directly. Use setActiveProvider() instead.');
 | 
			
		||||
@ -35,9 +36,9 @@ function upsert(uid, provider, settings) {
 | 
			
		||||
    
 | 
			
		||||
    // Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
 | 
			
		||||
    const stmt = db.prepare(`
 | 
			
		||||
        INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
 | 
			
		||||
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
        ON CONFLICT(uid, provider) DO UPDATE SET
 | 
			
		||||
        INSERT INTO provider_settings (provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
 | 
			
		||||
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
        ON CONFLICT(provider) DO UPDATE SET
 | 
			
		||||
            api_key = excluded.api_key,
 | 
			
		||||
            selected_llm_model = excluded.selected_llm_model,
 | 
			
		||||
            selected_stt_model = excluded.selected_stt_model,
 | 
			
		||||
@ -47,7 +48,6 @@ function upsert(uid, provider, settings) {
 | 
			
		||||
    `);
 | 
			
		||||
    
 | 
			
		||||
    const result = stmt.run(
 | 
			
		||||
        uid,
 | 
			
		||||
        provider,
 | 
			
		||||
        settings.api_key || null,
 | 
			
		||||
        settings.selected_llm_model || null,
 | 
			
		||||
@ -61,55 +61,55 @@ function upsert(uid, provider, settings) {
 | 
			
		||||
    return { changes: result.changes };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function remove(uid, provider) {
 | 
			
		||||
function remove(provider) {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ? AND provider = ?');
 | 
			
		||||
    const result = stmt.run(uid, provider);
 | 
			
		||||
    const stmt = db.prepare('DELETE FROM provider_settings WHERE provider = ?');
 | 
			
		||||
    const result = stmt.run(provider);
 | 
			
		||||
    return { changes: result.changes };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeAllByUid(uid) {
 | 
			
		||||
function removeAll() {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ?');
 | 
			
		||||
    const result = stmt.run(uid);
 | 
			
		||||
    const stmt = db.prepare('DELETE FROM provider_settings');
 | 
			
		||||
    const result = stmt.run();
 | 
			
		||||
    return { changes: result.changes };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRawApiKeysByUid(uid) {
 | 
			
		||||
function getRawApiKeys() {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const stmt = db.prepare('SELECT api_key FROM provider_settings WHERE uid = ?');
 | 
			
		||||
    return stmt.all(uid);
 | 
			
		||||
    const stmt = db.prepare('SELECT api_key FROM provider_settings');
 | 
			
		||||
    return stmt.all();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get active provider for a specific type (llm or stt)
 | 
			
		||||
function getActiveProvider(uid, type) {
 | 
			
		||||
function getActiveProvider(type) {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
 | 
			
		||||
    const stmt = db.prepare(`SELECT * FROM provider_settings WHERE uid = ? AND ${column} = 1`);
 | 
			
		||||
    const result = stmt.get(uid) || null;
 | 
			
		||||
    const stmt = db.prepare(`SELECT * FROM provider_settings WHERE ${column} = 1`);
 | 
			
		||||
    const result = stmt.get() || null;
 | 
			
		||||
    
 | 
			
		||||
    if (result && result.api_key) {
 | 
			
		||||
        result.api_key = result.api_key;
 | 
			
		||||
    if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
			
		||||
        result.api_key = encryptionService.decrypt(result.api_key);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set active provider for a specific type
 | 
			
		||||
function setActiveProvider(uid, provider, type) {
 | 
			
		||||
function setActiveProvider(provider, type) {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
 | 
			
		||||
    
 | 
			
		||||
    // Start transaction to ensure only one provider is active
 | 
			
		||||
    db.transaction(() => {
 | 
			
		||||
        // First, deactivate all providers for this type
 | 
			
		||||
        const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0 WHERE uid = ?`);
 | 
			
		||||
        deactivateStmt.run(uid);
 | 
			
		||||
        const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0`);
 | 
			
		||||
        deactivateStmt.run();
 | 
			
		||||
        
 | 
			
		||||
        // Then activate the specified provider
 | 
			
		||||
        if (provider) {
 | 
			
		||||
            const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE uid = ? AND provider = ?`);
 | 
			
		||||
            activateStmt.run(uid, provider);
 | 
			
		||||
            const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE provider = ?`);
 | 
			
		||||
            activateStmt.run(provider);
 | 
			
		||||
        }
 | 
			
		||||
    })();
 | 
			
		||||
    
 | 
			
		||||
@ -117,14 +117,14 @@ function setActiveProvider(uid, provider, type) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get all active settings (both llm and stt)
 | 
			
		||||
function getActiveSettings(uid) {
 | 
			
		||||
function getActiveSettings() {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const stmt = db.prepare(`
 | 
			
		||||
        SELECT * FROM provider_settings 
 | 
			
		||||
        WHERE uid = ? AND (is_active_llm = 1 OR is_active_stt = 1)
 | 
			
		||||
        WHERE (is_active_llm = 1 OR is_active_stt = 1)
 | 
			
		||||
        ORDER BY provider
 | 
			
		||||
    `);
 | 
			
		||||
    const results = stmt.all(uid);
 | 
			
		||||
    const results = stmt.all();
 | 
			
		||||
    
 | 
			
		||||
    // Decrypt API keys and organize by type
 | 
			
		||||
    const activeSettings = {
 | 
			
		||||
@ -133,8 +133,8 @@ function getActiveSettings(uid) {
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    results.forEach(result => {
 | 
			
		||||
        if (result.api_key) {
 | 
			
		||||
            result.api_key = result.api_key;
 | 
			
		||||
        if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
			
		||||
            result.api_key = encryptionService.decrypt(result.api_key);
 | 
			
		||||
        }
 | 
			
		||||
        if (result.is_active_llm) {
 | 
			
		||||
            activeSettings.llm = result;
 | 
			
		||||
@ -149,11 +149,11 @@ function getActiveSettings(uid) {
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getByProvider,
 | 
			
		||||
    getAllByUid,
 | 
			
		||||
    getAll,
 | 
			
		||||
    upsert,
 | 
			
		||||
    remove,
 | 
			
		||||
    removeAllByUid,
 | 
			
		||||
    getRawApiKeysByUid,
 | 
			
		||||
    removeAll,
 | 
			
		||||
    getRawApiKeys,
 | 
			
		||||
    getActiveProvider,
 | 
			
		||||
    setActiveProvider,
 | 
			
		||||
    getActiveSettings
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,6 @@ class AuthService {
 | 
			
		||||
        this.initializationPromise = null;
 | 
			
		||||
 | 
			
		||||
        sessionRepository.setAuthService(this);
 | 
			
		||||
        providerSettingsRepository.setAuthService(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initialize() {
 | 
			
		||||
@ -78,22 +77,21 @@ class AuthService {
 | 
			
		||||
                    // No 'await' here, so it runs in the background without blocking startup.
 | 
			
		||||
                    migrationService.checkAndRunMigration(user);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    // Start background task to fetch and save virtual key
 | 
			
		||||
                    (async () => {
 | 
			
		||||
                    // ***** CRITICAL: Wait for the virtual key and model state update to complete *****
 | 
			
		||||
                    try {
 | 
			
		||||
                        const idToken = await user.getIdToken(true);
 | 
			
		||||
                        const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
 | 
			
		||||
 | 
			
		||||
                        if (global.modelStateService) {
 | 
			
		||||
                                global.modelStateService.setFirebaseVirtualKey(virtualKey);
 | 
			
		||||
                            // The model state service now writes directly to the DB, no in-memory state.
 | 
			
		||||
                            await global.modelStateService.setFirebaseVirtualKey(virtualKey);
 | 
			
		||||
                        }
 | 
			
		||||
                            console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
 | 
			
		||||
                        console.log(`[AuthService] Virtual key for ${user.email} has been processed and state updated.`);
 | 
			
		||||
 | 
			
		||||
                    } catch (error) {
 | 
			
		||||
                            console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
 | 
			
		||||
                        console.error('[AuthService] Failed to fetch or save virtual key:', error);
 | 
			
		||||
                        // This is not critical enough to halt the login, but we should log it.
 | 
			
		||||
                    }
 | 
			
		||||
                    })();
 | 
			
		||||
 | 
			
		||||
                } else {
 | 
			
		||||
                    // User signed OUT
 | 
			
		||||
@ -101,7 +99,8 @@ class AuthService {
 | 
			
		||||
                    if (previousUser) {
 | 
			
		||||
                        console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
 | 
			
		||||
                        if (global.modelStateService) {
 | 
			
		||||
                            global.modelStateService.setFirebaseVirtualKey(null);
 | 
			
		||||
                            // The model state service now writes directly to the DB.
 | 
			
		||||
                            await global.modelStateService.setFirebaseVirtualKey(null);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    this.currentUser = null;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1066,7 +1066,7 @@ class OllamaService extends EventEmitter {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const selectedModels = modelStateService.getSelectedModels();
 | 
			
		||||
            const selectedModels = await modelStateService.getSelectedModels();
 | 
			
		||||
            const llmModelId = selectedModels.llm;
 | 
			
		||||
            
 | 
			
		||||
            // Check if it's an Ollama model
 | 
			
		||||
 | 
			
		||||
@ -106,6 +106,9 @@ class PermissionService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkKeychainCompleted(uid) {
 | 
			
		||||
    if (uid === "default_user") {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const completed = permissionRepository.checkKeychainCompleted(uid);
 | 
			
		||||
      console.log('[Permissions] Keychain completed status:', completed);
 | 
			
		||||
 | 
			
		||||
@ -40,8 +40,82 @@ class SQLiteClient {
 | 
			
		||||
        return `"${identifier}"`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _migrateProviderSettings() {
 | 
			
		||||
        const tablesInDb = this.getTablesFromDb();
 | 
			
		||||
        if (!tablesInDb.includes('provider_settings')) {
 | 
			
		||||
            return; // Table doesn't exist, no migration needed.
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        const providerSettingsInfo = this.db.prepare(`PRAGMA table_info(provider_settings)`).all();
 | 
			
		||||
        const hasUidColumn = providerSettingsInfo.some(col => col.name === 'uid');
 | 
			
		||||
    
 | 
			
		||||
        if (hasUidColumn) {
 | 
			
		||||
            console.log('[DB Migration] Old provider_settings schema detected. Starting robust migration...');
 | 
			
		||||
    
 | 
			
		||||
            try {
 | 
			
		||||
                this.db.transaction(() => {
 | 
			
		||||
                    this.db.exec('ALTER TABLE provider_settings RENAME TO provider_settings_old');
 | 
			
		||||
                    console.log('[DB Migration] Renamed provider_settings to provider_settings_old');
 | 
			
		||||
    
 | 
			
		||||
                    this.createTable('provider_settings', LATEST_SCHEMA.provider_settings);
 | 
			
		||||
                    console.log('[DB Migration] Created new provider_settings table');
 | 
			
		||||
    
 | 
			
		||||
                    // Dynamically build the migration query for robustness
 | 
			
		||||
                    const oldColumnNames = this.db.prepare(`PRAGMA table_info(provider_settings_old)`).all().map(c => c.name);
 | 
			
		||||
                    const newColumnNames = LATEST_SCHEMA.provider_settings.columns.map(c => c.name);
 | 
			
		||||
                    const commonColumns = newColumnNames.filter(name => oldColumnNames.includes(name));
 | 
			
		||||
    
 | 
			
		||||
                    if (!commonColumns.includes('provider')) {
 | 
			
		||||
                        console.warn('[DB Migration] Old table is missing the "provider" column. Aborting migration for this table.');
 | 
			
		||||
                        this.db.exec('DROP TABLE provider_settings_old');
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
    
 | 
			
		||||
                    const orderParts = [];
 | 
			
		||||
                    if (oldColumnNames.includes('updated_at')) orderParts.push('updated_at DESC');
 | 
			
		||||
                    if (oldColumnNames.includes('created_at')) orderParts.push('created_at DESC');
 | 
			
		||||
                    const orderByClause = orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
 | 
			
		||||
    
 | 
			
		||||
                    const columnsForInsert = commonColumns.map(c => this._validateAndQuoteIdentifier(c)).join(', ');
 | 
			
		||||
    
 | 
			
		||||
                    const migrationQuery = `
 | 
			
		||||
                        INSERT INTO provider_settings (${columnsForInsert})
 | 
			
		||||
                        SELECT ${columnsForInsert}
 | 
			
		||||
                        FROM (
 | 
			
		||||
                            SELECT *, ROW_NUMBER() OVER(PARTITION BY provider ${orderByClause}) as rn
 | 
			
		||||
                            FROM provider_settings_old
 | 
			
		||||
                        )
 | 
			
		||||
                        WHERE rn = 1
 | 
			
		||||
                    `;
 | 
			
		||||
                    
 | 
			
		||||
                    console.log(`[DB Migration] Executing robust migration query for columns: ${commonColumns.join(', ')}`);
 | 
			
		||||
                    const result = this.db.prepare(migrationQuery).run();
 | 
			
		||||
                    console.log(`[DB Migration] Migrated ${result.changes} rows to the new provider_settings table.`);
 | 
			
		||||
    
 | 
			
		||||
                    this.db.exec('DROP TABLE provider_settings_old');
 | 
			
		||||
                    console.log('[DB Migration] Dropped provider_settings_old table.');
 | 
			
		||||
                })();
 | 
			
		||||
                console.log('[DB Migration] provider_settings migration completed successfully.');
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('[DB Migration] Failed to migrate provider_settings table.', error);
 | 
			
		||||
                
 | 
			
		||||
                // Try to recover by dropping the temp table if it exists
 | 
			
		||||
                const oldTableExists = this.getTablesFromDb().includes('provider_settings_old');
 | 
			
		||||
                if (oldTableExists) {
 | 
			
		||||
                    this.db.exec('DROP TABLE provider_settings_old');
 | 
			
		||||
                    console.warn('[DB Migration] Cleaned up temporary old table after failure.');
 | 
			
		||||
                }
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async synchronizeSchema() {
 | 
			
		||||
        console.log('[DB Sync] Starting schema synchronization...');
 | 
			
		||||
 | 
			
		||||
        // Run special migration for provider_settings before the generic sync logic
 | 
			
		||||
        this._migrateProviderSettings();
 | 
			
		||||
 | 
			
		||||
        const tablesInDb = this.getTablesFromDb();
 | 
			
		||||
 | 
			
		||||
        for (const tableName of Object.keys(LATEST_SCHEMA)) {
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@ const { BrowserWindow } = require('electron');
 | 
			
		||||
const { spawn } = require('child_process');
 | 
			
		||||
const { createSTT } = require('../../common/ai/factory');
 | 
			
		||||
const modelStateService = require('../../common/services/modelStateService');
 | 
			
		||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager');
 | 
			
		||||
 | 
			
		||||
const COMPLETION_DEBOUNCE_MS = 2000;
 | 
			
		||||
 | 
			
		||||
@ -134,7 +133,7 @@ class SttService {
 | 
			
		||||
    async initializeSttSessions(language = 'en') {
 | 
			
		||||
        const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
 | 
			
		||||
 | 
			
		||||
        const modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        const modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        if (!modelInfo || !modelInfo.apiKey) {
 | 
			
		||||
            throw new Error('AI model or API key is not configured.');
 | 
			
		||||
        }
 | 
			
		||||
@ -467,7 +466,7 @@ class SttService {
 | 
			
		||||
        let modelInfo = this.modelInfo;
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
			
		||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
            modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        }
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            throw new Error('STT model info could not be retrieved.');
 | 
			
		||||
@ -492,7 +491,7 @@ class SttService {
 | 
			
		||||
        let modelInfo = this.modelInfo;
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
			
		||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
            modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        }
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            throw new Error('STT model info could not be retrieved.');
 | 
			
		||||
@ -578,7 +577,7 @@ class SttService {
 | 
			
		||||
        let modelInfo = this.modelInfo;
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
			
		||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
            modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        }
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            throw new Error('STT model info could not be retrieved.');
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ const { createLLM } = require('../../common/ai/factory');
 | 
			
		||||
const sessionRepository = require('../../common/repositories/session');
 | 
			
		||||
const summaryRepository = require('./repositories');
 | 
			
		||||
const modelStateService = require('../../common/services/modelStateService');
 | 
			
		||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js');
 | 
			
		||||
 | 
			
		||||
class SummaryService {
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -99,7 +98,7 @@ Please build upon this context while analyzing the new conversation segments.
 | 
			
		||||
                await sessionRepository.touch(this.currentSessionId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const modelInfo = modelStateService.getCurrentModelInfo('llm');
 | 
			
		||||
            const modelInfo = await modelStateService.getCurrentModelInfo('llm');
 | 
			
		||||
            if (!modelInfo || !modelInfo.apiKey) {
 | 
			
		||||
                throw new Error('AI model or API key is not configured.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -26,16 +26,14 @@ const NOTIFICATION_CONFIG = {
 | 
			
		||||
// New facade functions for model state management
 | 
			
		||||
async function getModelSettings() {
 | 
			
		||||
    try {
 | 
			
		||||
        const [config, storedKeys, selectedModels] = await Promise.all([
 | 
			
		||||
        const [config, storedKeys, selectedModels, availableLlm, availableStt] = await Promise.all([
 | 
			
		||||
            modelStateService.getProviderConfig(),
 | 
			
		||||
            modelStateService.getAllApiKeys(),
 | 
			
		||||
            modelStateService.getSelectedModels(),
 | 
			
		||||
            modelStateService.getAvailableModels('llm'),
 | 
			
		||||
            modelStateService.getAvailableModels('stt')
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        // 동기 함수들은 별도로 호출
 | 
			
		||||
        const availableLlm = modelStateService.getAvailableModels('llm');
 | 
			
		||||
        const availableStt = modelStateService.getAvailableModels('stt');
 | 
			
		||||
        
 | 
			
		||||
        return { success: true, data: { config, storedKeys, availableLlm, availableStt, selectedModels } };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('[SettingsService] Error getting model settings:', error);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										82
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								src/index.js
									
									
									
									
									
								
							@ -686,75 +686,43 @@ async function startWebStack() {
 | 
			
		||||
 | 
			
		||||
  console.log(`✅ API server started on http://localhost:${apiPort}`);
 | 
			
		||||
 | 
			
		||||
  console.log(`🚀 All services ready:`);
 | 
			
		||||
  console.log(`   Frontend: http://localhost:${frontendPort}`);
 | 
			
		||||
  console.log(`   API:      http://localhost:${apiPort}`);
 | 
			
		||||
  console.log(`🚀 All services ready:
 | 
			
		||||
   Frontend: http://localhost:${frontendPort}
 | 
			
		||||
   API:      http://localhost:${apiPort}`);
 | 
			
		||||
 | 
			
		||||
  return frontendPort;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Auto-update initialization
 | 
			
		||||
async function initAutoUpdater() {
 | 
			
		||||
    if (process.env.NODE_ENV === 'development') {
 | 
			
		||||
        console.log('Development environment, skipping auto-updater.');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const autoUpdateEnabled = await settingsService.getAutoUpdateSetting();
 | 
			
		||||
        if (!autoUpdateEnabled) {
 | 
			
		||||
            console.log('[AutoUpdater] Skipped because auto-updates are disabled in settings');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // Skip auto-updater in development mode
 | 
			
		||||
        if (!app.isPackaged) {
 | 
			
		||||
            console.log('[AutoUpdater] Skipped in development (app is not packaged)');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        autoUpdater.setFeedURL({
 | 
			
		||||
            provider: 'github',
 | 
			
		||||
            owner: 'pickle-com',
 | 
			
		||||
            repo: 'glass',
 | 
			
		||||
        await autoUpdater.checkForUpdates();
 | 
			
		||||
        autoUpdater.on('update-available', () => {
 | 
			
		||||
            console.log('Update available!');
 | 
			
		||||
            autoUpdater.downloadUpdate();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Immediately check for updates & notify
 | 
			
		||||
        autoUpdater.checkForUpdatesAndNotify()
 | 
			
		||||
            .catch(err => {
 | 
			
		||||
                console.error('[AutoUpdater] Error checking for updates:', err);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        autoUpdater.on('checking-for-update', () => {
 | 
			
		||||
            console.log('[AutoUpdater] Checking for updates…');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        autoUpdater.on('update-available', (info) => {
 | 
			
		||||
            console.log('[AutoUpdater] Update available:', info.version);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        autoUpdater.on('update-not-available', () => {
 | 
			
		||||
            console.log('[AutoUpdater] Application is up-to-date');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        autoUpdater.on('error', (err) => {
 | 
			
		||||
            console.error('[AutoUpdater] Error while updating:', err);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        autoUpdater.on('update-downloaded', (info) => {
 | 
			
		||||
            console.log(`[AutoUpdater] Update downloaded: ${info.version}`);
 | 
			
		||||
 | 
			
		||||
            const dialogOpts = {
 | 
			
		||||
        autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, date, url) => {
 | 
			
		||||
            console.log('Update downloaded:', releaseNotes, releaseName, date, url);
 | 
			
		||||
            dialog.showMessageBox({
 | 
			
		||||
                type: 'info',
 | 
			
		||||
                buttons: ['Install now', 'Install on next launch'],
 | 
			
		||||
                title: 'Update Available',
 | 
			
		||||
                message: 'A new version of Glass is ready to be installed.',
 | 
			
		||||
                defaultId: 0,
 | 
			
		||||
                cancelId: 1
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            dialog.showMessageBox(dialogOpts).then((returnValue) => {
 | 
			
		||||
                // returnValue.response 0 is for 'Install Now'
 | 
			
		||||
                if (returnValue.response === 0) {
 | 
			
		||||
                title: 'Application Update',
 | 
			
		||||
                message: `A new version of PickleGlass (${releaseName}) has been downloaded. It will be installed the next time you launch the application.`,
 | 
			
		||||
                buttons: ['Restart', 'Later']
 | 
			
		||||
            }).then(response => {
 | 
			
		||||
                if (response.response === 0) {
 | 
			
		||||
                    autoUpdater.quitAndInstall();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        console.error('[AutoUpdater] Failed to initialise:', e);
 | 
			
		||||
        autoUpdater.on('error', (err) => {
 | 
			
		||||
            console.error('Error in auto-updater:', err);
 | 
			
		||||
        });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        console.error('Error initializing auto-updater:', err);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -98,8 +98,6 @@ contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
    removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
 | 
			
		||||
    onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
 | 
			
		||||
    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback),
 | 
			
		||||
    onForceShowPermissionHeader: (callback) => ipcRenderer.on('force-show-permission-header', callback),
 | 
			
		||||
    removeOnForceShowPermissionHeader: (callback) => ipcRenderer.removeListener('force-show-permission-header', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/app/MainHeader.js
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,9 @@ class HeaderTransitionManager {
 | 
			
		||||
                console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
 | 
			
		||||
            } else if (type === 'permission') {
 | 
			
		||||
                this.permissionHeader = document.createElement('permission-setup');
 | 
			
		||||
                this.permissionHeader.addEventListener('request-resize', e => {
 | 
			
		||||
                    this._resizeForPermissionHeader(e.detail.height); 
 | 
			
		||||
                });
 | 
			
		||||
                this.permissionHeader.continueCallback = async () => {
 | 
			
		||||
                    if (window.api && window.api.headerController) {
 | 
			
		||||
                        console.log('[HeaderController] Re-initializing model state after permission grant...');
 | 
			
		||||
@ -198,7 +201,19 @@ class HeaderTransitionManager {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this._resizeForPermissionHeader();
 | 
			
		||||
        let initialHeight = 220;
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            try {
 | 
			
		||||
                const userState = await window.api.common.getCurrentUser();
 | 
			
		||||
                if (userState.mode === 'firebase') {
 | 
			
		||||
                    initialHeight = 280;
 | 
			
		||||
                }
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error('Could not get user state for resize', e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this._resizeForPermissionHeader(initialHeight);
 | 
			
		||||
        this.ensureHeader('permission');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -223,9 +238,10 @@ class HeaderTransitionManager {
 | 
			
		||||
        return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _resizeForPermissionHeader() {
 | 
			
		||||
    async _resizeForPermissionHeader(height) {
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        return window.api.headerController.resizeHeaderWindow({ width: 285, height: 220 })
 | 
			
		||||
        const finalHeight = height || 220;
 | 
			
		||||
        return window.api.headerController.resizeHeaderWindow({ width: 285, height: finalHeight })
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        .container {
 | 
			
		||||
            -webkit-app-region: drag;
 | 
			
		||||
            width: 285px;
 | 
			
		||||
            height: 220px;
 | 
			
		||||
            /* height is now set dynamically */
 | 
			
		||||
            padding: 18px 20px;
 | 
			
		||||
            background: rgba(0, 0, 0, 0.3);
 | 
			
		||||
            border-radius: 16px;
 | 
			
		||||
@ -103,6 +103,12 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
            margin-top: auto;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .form-content.all-granted {
 | 
			
		||||
            flex-grow: 1;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            margin-top: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .subtitle {
 | 
			
		||||
            color: rgba(255, 255, 255, 0.7);
 | 
			
		||||
            font-size: 11px;
 | 
			
		||||
@ -260,7 +266,8 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        screenGranted: { type: String },
 | 
			
		||||
        keychainGranted: { type: String },
 | 
			
		||||
        isChecking: { type: String },
 | 
			
		||||
        continueCallback: { type: Function }
 | 
			
		||||
        continueCallback: { type: Function },
 | 
			
		||||
        userMode: { type: String }, // 'local' or 'firebase'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -270,14 +277,47 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        this.keychainGranted = 'unknown';
 | 
			
		||||
        this.isChecking = false;
 | 
			
		||||
        this.continueCallback = null;
 | 
			
		||||
        this.userMode = 'local'; // Default to local
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updated(changedProperties) {
 | 
			
		||||
        super.updated(changedProperties);
 | 
			
		||||
        if (changedProperties.has('userMode')) {
 | 
			
		||||
            const newHeight = this.userMode === 'firebase' ? 280 : 220;
 | 
			
		||||
            console.log(`[PermissionHeader] User mode changed to ${this.userMode}, requesting resize to ${newHeight}px`);
 | 
			
		||||
            this.dispatchEvent(new CustomEvent('request-resize', {
 | 
			
		||||
                detail: { height: newHeight },
 | 
			
		||||
                bubbles: true,
 | 
			
		||||
                composed: true
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            try {
 | 
			
		||||
                const userState = await window.api.common.getCurrentUser();
 | 
			
		||||
                this.userMode = userState.mode;
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error('[PermissionHeader] Failed to get user state', e);
 | 
			
		||||
                this.userMode = 'local'; // Fallback to local
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.checkPermissions();
 | 
			
		||||
        
 | 
			
		||||
        // Set up periodic permission check
 | 
			
		||||
        this.permissionCheckInterval = setInterval(() => {
 | 
			
		||||
        this.permissionCheckInterval = setInterval(async () => {
 | 
			
		||||
            if (window.api) {
 | 
			
		||||
                try {
 | 
			
		||||
                    const userState = await window.api.common.getCurrentUser();
 | 
			
		||||
                    this.userMode = userState.mode;
 | 
			
		||||
                } catch (e) {
 | 
			
		||||
                    this.userMode = 'local';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            this.checkPermissions();
 | 
			
		||||
        }, 1000);
 | 
			
		||||
    }
 | 
			
		||||
@ -312,10 +352,13 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const isKeychainRequired = this.userMode === 'firebase';
 | 
			
		||||
            const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
 | 
			
		||||
            
 | 
			
		||||
            // if all permissions granted == automatically continue
 | 
			
		||||
            if (this.microphoneGranted === 'granted' && 
 | 
			
		||||
                this.screenGranted === 'granted' && 
 | 
			
		||||
                this.keychainGranted === 'granted' && 
 | 
			
		||||
                keychainOk && 
 | 
			
		||||
                this.continueCallback) {
 | 
			
		||||
                console.log('[PermissionHeader] All permissions granted, proceeding automatically');
 | 
			
		||||
                setTimeout(() => this.handleContinue(), 500);
 | 
			
		||||
@ -405,12 +448,15 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleContinue() {
 | 
			
		||||
        const isKeychainRequired = this.userMode === 'firebase';
 | 
			
		||||
        const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
 | 
			
		||||
 | 
			
		||||
        if (this.continueCallback && 
 | 
			
		||||
            this.microphoneGranted === 'granted' && 
 | 
			
		||||
            this.screenGranted === 'granted' && 
 | 
			
		||||
            this.keychainGranted === 'granted') {
 | 
			
		||||
            keychainOk) {
 | 
			
		||||
            // Mark permissions as completed
 | 
			
		||||
            if (window.api) {
 | 
			
		||||
            if (window.api && isKeychainRequired) {
 | 
			
		||||
                try {
 | 
			
		||||
                    await window.api.permissionHeader.markKeychainCompleted();
 | 
			
		||||
                    console.log('[PermissionHeader] Marked keychain as completed');
 | 
			
		||||
@ -431,10 +477,13 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && this.keychainGranted === 'granted';
 | 
			
		||||
        const isKeychainRequired = this.userMode === 'firebase';
 | 
			
		||||
        const containerHeight = isKeychainRequired ? 280 : 220;
 | 
			
		||||
        const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
 | 
			
		||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && keychainOk;
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="container">
 | 
			
		||||
            <div class="container" style="height: ${containerHeight}px">
 | 
			
		||||
                <button class="close-button" @click=${this.handleClose} title="Close application">
 | 
			
		||||
                    <svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
 | 
			
		||||
                        <path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />
 | 
			
		||||
@ -442,8 +491,9 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
                </button>
 | 
			
		||||
                <h1 class="title">Permission Setup Required</h1>
 | 
			
		||||
 | 
			
		||||
                <div class="form-content">
 | 
			
		||||
                    <div class="subtitle">Grant access to microphone, screen recording and keychain to continue</div>
 | 
			
		||||
                <div class="form-content ${allGranted ? 'all-granted' : ''}">
 | 
			
		||||
                    ${!allGranted ? html`
 | 
			
		||||
                        <div class="subtitle">Grant access to microphone, screen recording${isKeychainRequired ? ' and keychain' : ''} to continue</div>
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="permission-status">
 | 
			
		||||
                            <div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
 | 
			
		||||
@ -474,57 +524,59 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
                                `}
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            ${isKeychainRequired ? html`
 | 
			
		||||
                                <div class="permission-item ${this.keychainGranted === 'granted' ? 'granted' : ''}">
 | 
			
		||||
                                    ${this.keychainGranted === 'granted' ? html`
 | 
			
		||||
                                        <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
			
		||||
                                        </svg>
 | 
			
		||||
                                <span>Keychain ✓</span>
 | 
			
		||||
                                        <span>Data Encryption ✓</span>
 | 
			
		||||
                                    ` : html`
 | 
			
		||||
                                        <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                            <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.744 5.668l-1.649 1.652c-.63.63-1.706.19-1.706-.742V12.18a.75.75 0 00-1.5 0v2.696c0 .932-1.075 1.372-1.706.742l-1.649-1.652A6 6 0 112 8zm-4 0a.75.75 0 00.75-.75A3.75 3.75 0 018.25 4a.75.75 0 000 1.5 2.25 2.25 0 012.25 2.25.75.75 0 00.75.75z" clip-rule="evenodd" />
 | 
			
		||||
                                        </svg>
 | 
			
		||||
                                <span>Keychain Access</span>
 | 
			
		||||
                                        <span>Data Encryption</span>
 | 
			
		||||
                                    `}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ` : ''}
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                    ${this.microphoneGranted !== 'granted' ? html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="action-button" 
 | 
			
		||||
                            @click=${this.handleMicrophoneClick}
 | 
			
		||||
                            ?disabled=${this.microphoneGranted === 'granted'}
 | 
			
		||||
                        >
 | 
			
		||||
                            Grant Microphone Access
 | 
			
		||||
                            ${this.microphoneGranted === 'granted' ? 'Microphone Access Granted' : 'Grant Microphone Access'}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
 | 
			
		||||
                    ${this.screenGranted !== 'granted' ? html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="action-button" 
 | 
			
		||||
                            @click=${this.handleScreenClick}
 | 
			
		||||
                            ?disabled=${this.screenGranted === 'granted'}
 | 
			
		||||
                        >
 | 
			
		||||
                            Grant Screen Recording Access
 | 
			
		||||
                            ${this.screenGranted === 'granted' ? 'Screen Recording Granted' : 'Grant Screen Recording Access'}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
 | 
			
		||||
                    ${this.keychainGranted !== 'granted' ? html`
 | 
			
		||||
                        ${isKeychainRequired ? html`
 | 
			
		||||
                            <button 
 | 
			
		||||
                                class="action-button" 
 | 
			
		||||
                                @click=${this.handleKeychainClick}
 | 
			
		||||
                                ?disabled=${this.keychainGranted === 'granted'}
 | 
			
		||||
                            >
 | 
			
		||||
                            Grant Keychain Access
 | 
			
		||||
                                ${this.keychainGranted === 'granted' ? 'Encryption Enabled' : 'Enable Encryption'}
 | 
			
		||||
                            </button>
 | 
			
		||||
                        <div class="subtitle">System prompt will appear. Select 'Always Allow' for seamless access.</div>
 | 
			
		||||
                            <div class="subtitle" style="visibility: ${this.keychainGranted === 'granted' ? 'hidden' : 'visible'}">
 | 
			
		||||
                                Stores the key to encrypt your data. Press "<b>Always Allow</b>" to continue.
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ` : ''}
 | 
			
		||||
 | 
			
		||||
                    ${allGranted ? html`
 | 
			
		||||
                    ` : html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="continue-button" 
 | 
			
		||||
                            @click=${this.handleContinue}
 | 
			
		||||
                        >
 | 
			
		||||
                            Continue to Pickle Glass
 | 
			
		||||
                        </button>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
                    `}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
@ -918,6 +918,8 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        this.setupIpcListeners();
 | 
			
		||||
        this.setupWindowResize();
 | 
			
		||||
        this.loadAutoUpdateSetting();
 | 
			
		||||
        // Force one height calculation immediately (innerHeight may be 0 at first)
 | 
			
		||||
        setTimeout(() => this.updateScrollHeight(), 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
@ -1030,8 +1032,10 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateScrollHeight() {
 | 
			
		||||
        const windowHeight = window.innerHeight;
 | 
			
		||||
        const maxHeight = windowHeight;
 | 
			
		||||
        // Electron 일부 시점에서 window.innerHeight 가 0 으로 보고되는 버그 보호
 | 
			
		||||
        const rawHeight = window.innerHeight || (window.screen ? window.screen.height : 0);
 | 
			
		||||
        const MIN_HEIGHT = 300; // 최소 보장 높이
 | 
			
		||||
        const maxHeight = Math.max(MIN_HEIGHT, rawHeight);
 | 
			
		||||
 | 
			
		||||
        this.style.maxHeight = `${maxHeight}px`;
 | 
			
		||||
 | 
			
		||||
@ -1043,6 +1047,8 @@ export class SettingsView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    handleMouseEnter = () => {
 | 
			
		||||
        window.api.settingsView.cancelHideSettingsWindow();
 | 
			
		||||
        // Recalculate height in case it was set to 0 before
 | 
			
		||||
        this.updateScrollHeight();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseLeave = () => {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user