diff --git a/backend/api/config.py b/backend/api/config.py
index df9bd09..151c8d6 100644
--- a/backend/api/config.py
+++ b/backend/api/config.py
@@ -14,6 +14,7 @@ import logging
from database import get_db
from auth import get_current_user, get_all_api_keys, create_api_key, revoke_api_key, delete_api_key
+from services.central_settings import get_anthropic_credentials
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -1228,7 +1229,6 @@ class AIInsightsSettingsResponse(BaseModel):
class AIInsightsSettingsUpdate(BaseModel):
enabled: Optional[bool] = None
- api_key: Optional[str] = None
model: Optional[str] = None
schedule_time: Optional[str] = None
daily_token_budget: Optional[int] = None
@@ -1239,23 +1239,21 @@ async def get_ai_insights_settings(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
- """Get AI Insights configuration."""
+ """Get AI Insights configuration. The Anthropic API key itself is managed
+ centrally in Portal → Settings → Integrations, not stored here."""
result = await db.execute(
text("""
- SELECT config_key, config_value, COALESCE(is_encrypted, false) as is_encrypted
+ SELECT config_key, config_value
FROM system_config
WHERE config_key LIKE 'ai_insights_%'
""")
)
- config = {}
- for row in result.fetchall():
- config[row.config_key] = row.config_value
- if row.config_key == 'ai_insights_api_key':
- config['_api_key_set'] = bool(row.config_value)
+ config = {row.config_key: row.config_value for row in result.fetchall()}
+ anthropic_creds = await get_anthropic_credentials()
return AIInsightsSettingsResponse(
enabled=config.get('ai_insights_enabled', 'false').lower() in ('true', '1', 'yes'),
- api_key_set=config.get('_api_key_set', False),
+ api_key_set=bool(anthropic_creds and anthropic_creds.get('api_key')),
model=config.get('ai_insights_model', 'claude-haiku-4-5-20251001'),
schedule_time=config.get('ai_insights_schedule_time', '07:15'),
daily_token_budget=int(config.get('ai_insights_daily_token_budget', '12000')),
@@ -1268,12 +1266,11 @@ async def update_ai_insights_settings(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
- """Update AI Insights configuration."""
+ """Update AI Insights configuration. Does not accept an API key —
+ manage that centrally in Portal → Settings → Integrations."""
updates = {}
if settings.enabled is not None:
updates['ai_insights_enabled'] = ('true' if settings.enabled else 'false', False)
- if settings.api_key is not None and settings.api_key.strip():
- updates['ai_insights_api_key'] = (simple_encrypt(settings.api_key.strip()), True)
if settings.model is not None:
updates['ai_insights_model'] = (settings.model, False)
if settings.schedule_time is not None:
@@ -1301,10 +1298,11 @@ async def test_ai_insights_connection(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
- """Test Anthropic API connection with stored API key."""
- api_key = await _get_config_value(db, "ai_insights_api_key")
+ """Test Anthropic API connection using the centrally configured API key."""
+ anthropic_creds = await get_anthropic_credentials()
+ api_key = anthropic_creds.get('api_key') if anthropic_creds else None
if not api_key:
- raise HTTPException(status_code=400, detail="No API key configured")
+ raise HTTPException(status_code=400, detail="Anthropic API key not configured — add it in Portal → Settings → Integrations")
model = await _get_config_value(db, "ai_insights_model") or "claude-haiku-4-5-20251001"
diff --git a/backend/jobs/ai_insights.py b/backend/jobs/ai_insights.py
index 7f1663c..5111b82 100644
--- a/backend/jobs/ai_insights.py
+++ b/backend/jobs/ai_insights.py
@@ -17,6 +17,7 @@ from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from database import AsyncSessionLocal
+from services.central_settings import get_anthropic_credentials
logger = logging.getLogger(__name__)
@@ -533,10 +534,11 @@ async def generate_insight(db: AsyncSession, triggered_by: str = "scheduler") ->
if config.get('ai_insights_enabled', 'false').lower() not in ('true', '1', 'yes'):
return {"success": False, "error": "AI insights disabled"}
- # Check API key
- api_key = config.get('ai_insights_api_key')
+ # Check API key — managed centrally in Portal → Settings → Integrations, not app-local
+ anthropic_creds = await get_anthropic_credentials()
+ api_key = anthropic_creds.get('api_key') if anthropic_creds else None
if not api_key:
- return {"success": False, "error": "No API key configured"}
+ return {"success": False, "error": "Anthropic API key not configured — add it in Portal → Settings → Integrations"}
model = config.get('ai_insights_model', DEFAULT_MODEL)
budget = int(config.get('ai_insights_daily_token_budget', str(DEFAULT_DAILY_TOKEN_BUDGET)))
diff --git a/backend/services/central_settings.py b/backend/services/central_settings.py
index e7326ff..09c9044 100644
--- a/backend/services/central_settings.py
+++ b/backend/services/central_settings.py
@@ -117,3 +117,17 @@ async def get_resos_credentials() -> Optional[dict]:
def get_resos_credentials_sync() -> Optional[dict]:
"""Blocking variant of get_resos_credentials for sync job contexts."""
return _extract_resos(get_integration_sync("resos"))
+
+
+def _extract_anthropic(s: Optional[dict]) -> Optional[dict]:
+ if not s:
+ return None
+ key = s.get("api_key") or ""
+ if not key:
+ return None
+ return {"api_key": key}
+
+
+async def get_anthropic_credentials() -> Optional[dict]:
+ """Returns {'api_key'} from central settings, or None if not configured."""
+ return _extract_anthropic(await get_integration("anthropic"))
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1e6adc2..f92dee8 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,7 @@
import { Routes, Route, Navigate } from 'react-router-dom'
import AuthGate from './components/AuthGate'
+import { UpdateBanner } from './components/UpdateBanner'
+import { useVersionCheck } from './hooks/useVersionCheck'
import Layout from './components/Layout'
import Dashboard from './pages/Dashboard'
import Forecasts from './pages/Forecasts'
@@ -8,10 +10,12 @@ import Accuracy from './pages/Accuracy'
import Settings from './pages/Settings'
export default function App() {
+ const updateAvailable = useVersionCheck('/forecasting/health')
return (
-