Forecasting app: hybrid port to HNF stack
Python FastAPI ML backend kept intact; auth replaced with central hnf_session cookie verification. Frontend rebuilt on React 18 + TS + Vite with stack design system, Plotly charts retained. Shared Postgres via DATABASE_URL; schema applied on startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
75d2c1fa9d
103 changed files with 70316 additions and 0 deletions
47
frontend/src/components/AuthGate.tsx
Normal file
47
frontend/src/components/AuthGate.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { User } from '../types'
|
||||
|
||||
interface AuthCtx { user: User }
|
||||
const Ctx = createContext<AuthCtx | null>(null)
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(Ctx)
|
||||
if (!ctx) throw new Error('useAuth outside AuthGate')
|
||||
return ctx
|
||||
}
|
||||
|
||||
export default function AuthGate({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [checking, setChecking] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/forecasting/api/auth/verify?app=forecasting', { credentials: 'include' })
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('unauth')
|
||||
return r.json()
|
||||
})
|
||||
.then(data => setUser({
|
||||
email: data.email || data.sub || '',
|
||||
name: data.name || data.display_name || '',
|
||||
is_admin: data.is_admin ?? false,
|
||||
caps: data.caps ?? [],
|
||||
}))
|
||||
.catch(() => {
|
||||
window.location.href = '/auth/login?redirect=' + encodeURIComponent(window.location.pathname)
|
||||
})
|
||||
.finally(() => setChecking(false))
|
||||
}, [])
|
||||
|
||||
if (checking) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--navy-dark)' }}>
|
||||
<div className="spinner" style={{ width: 32, height: 32 }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) return null
|
||||
|
||||
return <Ctx.Provider value={{ user }}>{children}</Ctx.Provider>
|
||||
}
|
||||
59
frontend/src/components/Layout.tsx
Normal file
59
frontend/src/components/Layout.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { NavLink } from 'react-router-dom'
|
||||
import {
|
||||
TrendingUp, BarChart2, Calendar, Target, Globe, History,
|
||||
Settings, Bot,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from './AuthGate'
|
||||
import { can } from '../types'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
const ICON = { size: 16, strokeWidth: 1.75 }
|
||||
|
||||
const NAV = [
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: Bot, cap: 'view' },
|
||||
{ to: '/forecasts', label: 'Forecasts', icon: TrendingUp, cap: 'view' },
|
||||
{ to: '/history', label: 'History', icon: History, cap: 'view' },
|
||||
{ to: '/bookability', label: 'Bookability', icon: Calendar, cap: 'view_bookability' },
|
||||
{ to: '/competitor-rates', label: 'Competitors', icon: Globe, cap: 'view_competitor_rates' },
|
||||
{ to: '/accuracy', label: 'Accuracy', icon: Target, cap: 'view_accuracy' },
|
||||
{ to: '/settings', label: 'Settings', icon: Settings, cap: 'settings' },
|
||||
]
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const { user } = useAuth()
|
||||
const items = NAV.filter(n => can(user, n.cap))
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-logo">
|
||||
<BarChart2 size={18} strokeWidth={1.75} />
|
||||
Forecasting
|
||||
</div>
|
||||
<nav className="sidebar-nav">
|
||||
{items.map(({ to, label, icon: Icon }) => (
|
||||
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
|
||||
<Icon {...ICON} />
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="sidebar-user">{user.name || user.email}</div>
|
||||
</aside>
|
||||
|
||||
<header className="top-bar">
|
||||
<BarChart2 size={18} strokeWidth={1.75} color="var(--gold)" />
|
||||
<span className="top-bar-title">Forecasting</span>
|
||||
<nav className="top-bar-nav">
|
||||
{items.map(({ to, label }) => (
|
||||
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main className="page-content">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue