Use central Anthropic key for AI insights; add update-check banner

AI insights now fetches the Claude API key from the central settings
service instead of storing its own encrypted copy, so wages and other
apps can share the same key. Also wires up the update-available banner
using the existing version-check hook pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-24 20:43:03 +00:00
parent 72f607132c
commit 4137813fc7
7 changed files with 141 additions and 42 deletions

View file

@ -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 (
<AuthGate>
<Layout>
<Routes>
<>
<AuthGate>
<Layout>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/forecasts" element={<Forecasts />} />
@ -23,8 +27,10 @@ export default function App() {
<Route path="/settings" element={<Settings />} />
<Route path="/settings/:tab" element={<Settings />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Layout>
</AuthGate>
</Routes>
</Layout>
</AuthGate>
<UpdateBanner visible={updateAvailable} />
</>
)
}

View file

@ -0,0 +1,44 @@
import { RefreshCw } from 'lucide-react'
export function UpdateBanner({ visible }: { visible: boolean }) {
if (!visible) return null
return (
<div style={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
zIndex: 9999,
background: 'var(--sidebar)',
color: 'var(--text-light)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '12px',
padding: '10px 16px',
fontSize: '14px',
boxShadow: '0 -2px 8px rgba(0,0,0,0.3)',
}}>
<span>A new version is available.</span>
<button
onClick={() => window.location.reload()}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
background: 'var(--accent)',
color: 'var(--sidebar)',
border: 'none',
borderRadius: '4px',
padding: '6px 14px',
fontWeight: 600,
cursor: 'pointer',
fontSize: '13px',
}}
>
<RefreshCw size={14} strokeWidth={1.75} />
Reload
</button>
</div>
)
}

View file

@ -0,0 +1,43 @@
import { useEffect, useState } from 'react'
const POLL_MS = 2 * 60 * 1000
export function useVersionCheck(healthUrl: string) {
const [updateAvailable, setUpdateAvailable] = useState(false)
useEffect(() => {
let seenVersion: string | null = null
async function check() {
try {
const res = await fetch(healthUrl, { cache: 'no-store' })
if (!res.ok) return
const data = await res.json()
const v: string | undefined = data.version
if (!v) return
if (seenVersion === null) {
seenVersion = v
} else if (v !== seenVersion) {
setUpdateAvailable(true)
}
} catch {
// network error — skip silently
}
}
check()
const interval = setInterval(check, POLL_MS)
function onVisible() {
if (document.visibilityState === 'visible') check()
}
document.addEventListener('visibilitychange', onVisible)
return () => {
clearInterval(interval)
document.removeEventListener('visibilitychange', onVisible)
}
}, [healthUrl])
return updateAvailable
}

View file

@ -6049,7 +6049,6 @@ interface AIInsightsSettings {
const AIInsightsPage: React.FC = () => {
const queryClient = useQueryClient()
const [apiKey, setApiKey] = useState('')
const [model, setModel] = useState('claude-haiku-4-5-20251001')
const [scheduleTime, setScheduleTime] = useState('07:15')
const [enabled, setEnabled] = useState(false)
@ -6096,9 +6095,6 @@ const AIInsightsPage: React.FC = () => {
schedule_time: scheduleTime,
daily_token_budget: tokenBudget,
}
if (apiKey.trim()) {
body.api_key = apiKey.trim()
}
const response = await fetch('/forecasting/api/config/settings/ai-insights', {
method: 'POST',
headers: {
@ -6108,7 +6104,6 @@ const AIInsightsPage: React.FC = () => {
})
if (response.ok) {
setSaveStatus('saved')
setApiKey('')
queryClient.invalidateQueries({ queryKey: ['ai-insights-settings'] })
setTimeout(() => setSaveStatus('idle'), 3000)
} else {
@ -6194,22 +6189,19 @@ const AIInsightsPage: React.FC = () => {
</label>
</div>
{/* API Key */}
{/* API Key — managed centrally, not per-app */}
<div style={styles.formRow}>
<div style={styles.formField}>
<label style={styles.label}>Anthropic API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={settings?.api_key_set ? '•••••••• (key saved)' : 'sk-ant-...'}
style={styles.input}
/>
{settings?.api_key_set && (
<span style={{ fontSize: typography.xs, color: colors.success, marginTop: spacing.xs, display: 'block' }}>
API key is configured
</span>
)}
<span style={{
fontSize: typography.sm,
color: settings?.api_key_set ? colors.success : colors.error,
display: 'block',
}}>
{settings?.api_key_set
? 'Using centrally configured Anthropic key'
: 'Not configured'} manage it in Portal Settings Integrations
</span>
</div>
</div>