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:
parent
72f607132c
commit
4137813fc7
7 changed files with 141 additions and 42 deletions
|
|
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
44
frontend/src/components/UpdateBanner.tsx
Normal file
44
frontend/src/components/UpdateBanner.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
43
frontend/src/hooks/useVersionCheck.ts
Normal file
43
frontend/src/hooks/useVersionCheck.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue