personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve settings app providers UX and API key status display

- Check API keys via os.getenv() to reflect true runtime availability
(shell env + .env file), not just journal config
- Show "(system)" indicator for keys available from system environment
- Add save feedback checkmarks to provider/tier detail task overrides
- Rename tiers to Best/Average/Fast, section to "Detailed Tasks"
- Pre-select built-in tier defaults when they differ from user default
- Make nav sticky below facet bar, improve active item visibility
- Consolidate API_KEY_ENV_VARS constant, remove unused CSS classes
- Fix updateEnvStatus() missing argument bug

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+129 -53
+23 -10
apps/settings/routes.py
··· 5 5 6 6 import copy 7 7 import json 8 + import os 8 9 import re 9 10 from pathlib import Path 10 11 from typing import Any ··· 22 23 ) 23 24 24 25 26 + # API keys that can be configured in the env section 27 + # Used for system env checks and allowed env fields validation 28 + API_KEY_ENV_VARS = [ 29 + "GOOGLE_API_KEY", 30 + "ANTHROPIC_API_KEY", 31 + "OPENAI_API_KEY", 32 + "REVAI_ACCESS_TOKEN", 33 + ] 34 + 35 + 25 36 @settings_bp.route("/api/config") 26 37 def get_config() -> Any: 27 38 """Return the journal configuration. 28 39 29 40 The env section is masked for security - returns boolean indicating 30 41 whether each key is configured rather than the actual values. 42 + 43 + Also returns system_env with boolean status for keys available from 44 + the system environment (shell env + .env file). 31 45 """ 32 46 try: 33 47 config = get_journal_config() 34 - # Mask env values - return True/False for whether key is set 48 + # Mask env values - return True/False for whether key is set in journal config 35 49 if "env" in config: 36 50 config["env"] = {k: bool(v) for k, v in config["env"].items()} 51 + 52 + # Add system_env - keys available from os.getenv (shell + .env) 53 + config["system_env"] = {k: bool(os.getenv(k)) for k in API_KEY_ENV_VARS} 54 + 37 55 return jsonify(config) 38 56 except Exception as e: 39 57 return jsonify({"error": str(e)}), 500 ··· 82 100 ], 83 101 "transcribe": ["device", "model", "compute_type"], 84 102 "convey": ["password"], 85 - "env": [ 86 - "GOOGLE_API_KEY", 87 - "ANTHROPIC_API_KEY", 88 - "OPENAI_API_KEY", 89 - "REVAI_ACCESS_TOKEN", 90 - ], 103 + "env": API_KEY_ENV_VARS, 91 104 } 92 105 93 106 if section not in allowed_sections: ··· 183 196 184 197 config = get_journal_config() 185 198 providers_config = config.get("providers", {}) 186 - env_config = config.get("env", {}) 187 199 188 200 # Get default settings 189 201 default = providers_config.get("default", {}) ··· 202 214 "group": ctx_config["group"], 203 215 } 204 216 205 - # Check API key status for each provider 217 + # Check API key status for each provider using os.getenv() 218 + # This reflects the true runtime availability (shell env + .env + journal config) 206 219 api_keys = {} 207 220 for provider, env_key in PROVIDER_API_KEYS.items(): 208 - api_keys[provider] = bool(env_config.get(env_key)) 221 + api_keys[provider] = bool(os.getenv(env_key)) 209 222 210 223 return jsonify( 211 224 {
+106 -43
apps/settings/workspace.html
··· 13 13 .settings-nav { 14 14 width: 200px; 15 15 flex-shrink: 0; 16 + position: sticky; 17 + top: calc(var(--facet-bar-height, 60px) + 1em); 18 + align-self: flex-start; 19 + max-height: calc(100vh - var(--facet-bar-height, 60px) - 2em); 20 + overflow-y: auto; 16 21 } 17 22 18 23 .settings-nav-group { ··· 48 53 } 49 54 50 55 .settings-nav-item.active { 51 - background: var(--facet-bg, #e8f0fe); 52 - color: var(--facet-color, #1a73e8); 53 - font-weight: 500; 56 + background: var(--facet-color, #1a73e8); 57 + color: white; 58 + font-weight: 600; 54 59 } 55 60 56 61 /* Mobile nav dropdown */ ··· 171 176 172 177 .settings-field small.status-fade { 173 178 opacity: 0; 179 + } 180 + 181 + /* API key status indicators */ 182 + .key-status { 183 + font-weight: normal; 184 + margin-left: 0.3em; 185 + } 186 + 187 + .key-status-journal { 188 + color: #28a745; 189 + } 190 + 191 + .key-status-system { 192 + color: #6c757d; 193 + font-size: 0.85em; 174 194 } 175 195 176 196 /* Password field with toggle */ ··· 742 762 gap: 0.5em; 743 763 } 744 764 745 - .context-inherited { 746 - font-size: 0.85em; 747 - color: #888; 748 - font-style: italic; 749 - } 750 - 751 - .context-override-badge { 752 - font-size: 0.75em; 753 - padding: 0.2em 0.5em; 754 - background: var(--facet-bg, #e8f0fe); 755 - color: var(--facet-color, #1a73e8); 756 - border-radius: 4px; 757 - font-weight: 500; 758 - } 759 - 760 765 .context-select { 761 766 padding: 0.3em 0.5em; 762 767 border: 1px solid #ddd; ··· 764 769 font-size: 0.85em; 765 770 background: white; 766 771 min-width: 80px; 772 + } 773 + 774 + .context-action { 775 + width: 1.5em; 776 + text-align: center; 767 777 } 768 778 769 779 .context-reset { ··· 785 795 .context-reset:hover { 786 796 color: #d32f2f; 787 797 background: #ffebee; 798 + } 799 + 800 + .context-saved { 801 + color: #28a745; 802 + opacity: 1; 803 + transition: opacity 0.3s; 804 + } 805 + 806 + .context-saved.fade { 807 + opacity: 0; 788 808 } 789 809 790 810 /* Setup section */ ··· 996 1016 <option value="openai">OpenAI (GPT)</option> 997 1017 <option value="anthropic">Anthropic (Claude)</option> 998 1018 </select> 1019 + <small></small> 999 1020 </div> 1000 1021 <div class="settings-field"> 1001 1022 <label for="field-tier">Default Tier</label> 1002 1023 <select id="field-tier"> 1003 - <option value="1">Pro - Most capable</option> 1004 - <option value="2">Flash - Balanced</option> 1005 - <option value="3">Lite - Fastest</option> 1024 + <option value="1">Best - Most capable</option> 1025 + <option value="2">Average - Balanced</option> 1026 + <option value="3">Fast - Lightweight</option> 1006 1027 </select> 1028 + <small></small> 1007 1029 </div> 1008 1030 </div> 1009 1031 <div id="providerKeyWarning" class="provider-key-warning" style="display:none"> ··· 1013 1035 </div> 1014 1036 <small id="providerStatus">Loading...</small> 1015 1037 1016 - <!-- Context Overrides --> 1038 + <!-- Detailed Tasks --> 1017 1039 <div class="context-overrides" id="contextOverrides"> 1018 1040 <div class="context-overrides-header" id="contextOverridesHeader"> 1019 - <span class="context-overrides-title">Context Overrides</span> 1041 + <span class="context-overrides-title">Detailed Tasks</span> 1020 1042 <span class="context-overrides-toggle">&#9660;</span> 1021 1043 </div> 1022 1044 <div class="context-overrides-body" id="contextOverridesBody"> 1023 - <p style="color:#666;font-size:0.9em;margin:0 0 1em 0">Override provider or tier for specific tasks. Unset values inherit from defaults.</p> 1045 + <p style="color:#666;font-size:0.9em;margin:0 0 1em 0">Override provider or tier for specific tasks. Unset values use defaults.</p> 1024 1046 <div id="contextGroups">Loading...</div> 1025 1047 </div> 1026 1048 </div> ··· 1355 1377 const convey = config.convey || {}; 1356 1378 setValue('field-password', convey.password || ''); 1357 1379 1358 - // Env (API keys) - show status indicators, values are masked as booleans 1380 + // Env (API keys) - show status indicators 1381 + // env = journal config status, system_env = shell/.env status 1359 1382 const env = config.env || {}; 1360 - updateEnvStatus('field-env-google', env.GOOGLE_API_KEY); 1361 - updateEnvStatus('field-env-openai', env.OPENAI_API_KEY); 1362 - updateEnvStatus('field-env-anthropic', env.ANTHROPIC_API_KEY); 1363 - updateEnvStatus('field-env-revai', env.REVAI_ACCESS_TOKEN); 1383 + const sysEnv = config.system_env || {}; 1384 + updateEnvStatus('field-env-google', env.GOOGLE_API_KEY, sysEnv.GOOGLE_API_KEY); 1385 + updateEnvStatus('field-env-openai', env.OPENAI_API_KEY, sysEnv.OPENAI_API_KEY); 1386 + updateEnvStatus('field-env-anthropic', env.ANTHROPIC_API_KEY, sysEnv.ANTHROPIC_API_KEY); 1387 + updateEnvStatus('field-env-revai', env.REVAI_ACCESS_TOKEN, sysEnv.REVAI_ACCESS_TOKEN); 1364 1388 } 1365 1389 1366 - function updateEnvStatus(fieldId, isConfigured) { 1390 + function updateEnvStatus(fieldId, isJournalConfigured, isSystemConfigured) { 1367 1391 const field = document.getElementById(fieldId); 1368 1392 if (!field) return; 1369 1393 1370 1394 // Update placeholder to indicate status 1371 - if (isConfigured) { 1395 + if (isJournalConfigured) { 1372 1396 field.placeholder = 'Key configured (enter new value to replace)'; 1397 + } else if (isSystemConfigured) { 1398 + field.placeholder = 'Key available from system environment'; 1373 1399 } else { 1374 1400 field.placeholder = 'Enter API key'; 1375 1401 } ··· 1381 1407 const existingStatus = label.querySelector('.key-status'); 1382 1408 if (existingStatus) existingStatus.remove(); 1383 1409 1384 - if (isConfigured) { 1410 + if (isJournalConfigured) { 1385 1411 const status = document.createElement('span'); 1386 - status.className = 'key-status'; 1412 + status.className = 'key-status key-status-journal'; 1387 1413 status.textContent = ' \u2713'; 1388 - status.style.color = '#28a745'; 1414 + label.appendChild(status); 1415 + } else if (isSystemConfigured) { 1416 + const status = document.createElement('span'); 1417 + status.className = 'key-status key-status-system'; 1418 + status.textContent = ' (system)'; 1389 1419 label.appendChild(status); 1390 1420 } 1391 1421 } ··· 1452 1482 // For env fields, clear the input and update status indicator 1453 1483 if (section === 'env') { 1454 1484 el.value = ''; 1455 - updateEnvStatus(el.id, true); 1485 + updateEnvStatus(el.id, true, true); 1456 1486 } 1457 1487 } else { 1458 1488 throw new Error(result.error); ··· 1920 1950 const container = document.getElementById('contextGroups'); 1921 1951 const contexts = data.context_defaults; 1922 1952 const overrides = data.contexts || {}; 1953 + const defaultTier = data.default.tier; 1923 1954 1924 1955 // Group by group name 1925 1956 const groups = {}; ··· 1938 1969 for (const item of items) { 1939 1970 const override = overrides[item.pattern]; 1940 1971 const hasOverride = !!override; 1972 + // Check if built-in tier differs from user's default tier 1973 + const builtInTier = item.tier; 1974 + const showBuiltIn = !override?.tier && builtInTier !== defaultTier; 1941 1975 1942 1976 html += `<div class="context-item" data-pattern="${item.pattern}">`; 1943 1977 html += `<span class="context-label">${item.label}</span>`; ··· 1945 1979 1946 1980 // Provider select 1947 1981 html += `<select class="context-select context-provider" data-pattern="${item.pattern}">`; 1948 - html += `<option value="">Inherit</option>`; 1982 + html += `<option value="">Default</option>`; 1949 1983 html += `<option value="google" ${override?.provider === 'google' ? 'selected' : ''}>Google</option>`; 1950 1984 html += `<option value="openai" ${override?.provider === 'openai' ? 'selected' : ''}>OpenAI</option>`; 1951 1985 html += `<option value="anthropic" ${override?.provider === 'anthropic' ? 'selected' : ''}>Anthropic</option>`; 1952 1986 html += `</select>`; 1953 1987 1954 - // Tier select 1988 + // Tier select - show built-in tier if it differs from default 1955 1989 html += `<select class="context-select context-tier" data-pattern="${item.pattern}">`; 1956 - html += `<option value="">Inherit</option>`; 1957 - html += `<option value="1" ${override?.tier === 1 ? 'selected' : ''}>Pro</option>`; 1958 - html += `<option value="2" ${override?.tier === 2 ? 'selected' : ''}>Flash</option>`; 1959 - html += `<option value="3" ${override?.tier === 3 ? 'selected' : ''}>Lite</option>`; 1990 + html += `<option value="">Default</option>`; 1991 + html += `<option value="1" ${override?.tier === 1 || (showBuiltIn && builtInTier === 1) ? 'selected' : ''}>Best</option>`; 1992 + html += `<option value="2" ${override?.tier === 2 || (showBuiltIn && builtInTier === 2) ? 'selected' : ''}>Average</option>`; 1993 + html += `<option value="3" ${override?.tier === 3 || (showBuiltIn && builtInTier === 3) ? 'selected' : ''}>Fast</option>`; 1960 1994 html += `</select>`; 1961 1995 1962 - // Reset button (only shown if has override) 1996 + // Action area (reset button or save indicator) 1997 + html += `<span class="context-action" data-pattern="${item.pattern}">`; 1963 1998 if (hasOverride) { 1964 1999 html += `<button class="context-reset" data-pattern="${item.pattern}" title="Reset to default">&times;</button>`; 1965 2000 } 2001 + html += `</span>`; 1966 2002 1967 2003 html += `</div></div>`; 1968 2004 } ··· 2024 2060 if (result.error) throw new Error(result.error); 2025 2061 2026 2062 providersData = result; 2027 - renderContextGroups(result); 2063 + 2064 + // Show save feedback and update action area 2065 + const actionArea = document.querySelector(`.context-action[data-pattern="${pattern}"]`); 2066 + if (actionArea) { 2067 + const hasOverride = !!(result.contexts && result.contexts[pattern]); 2068 + if (remove) { 2069 + // Re-render needed when resetting to restore built-in tier selection 2070 + renderContextGroups(result); 2071 + } else { 2072 + // Show checkmark, then update to show/hide reset button 2073 + actionArea.innerHTML = `<span class="context-saved">\u2713</span>`; 2074 + setTimeout(() => { 2075 + const saved = actionArea.querySelector('.context-saved'); 2076 + if (saved) saved.classList.add('fade'); 2077 + setTimeout(() => { 2078 + if (hasOverride) { 2079 + actionArea.innerHTML = `<button class="context-reset" data-pattern="${pattern}" title="Reset to default">&times;</button>`; 2080 + // Re-attach click handler for the new reset button 2081 + actionArea.querySelector('.context-reset')?.addEventListener('click', () => { 2082 + saveContextOverride(pattern, null, null, true); 2083 + }); 2084 + } else { 2085 + actionArea.innerHTML = ''; 2086 + } 2087 + }, 300); 2088 + }, 1000); 2089 + } 2090 + } 2028 2091 } catch (err) { 2029 2092 console.error('Error saving context override:', err); 2030 2093 notifyError('Save Failed', err.message);