feat: add frontend + backend + database to retrieve and compute news from Yahoo

This commit is contained in:
2026-04-18 23:53:57 +02:00
parent f9b6d35c49
commit 93668273ff
84 changed files with 15431 additions and 0 deletions

52
frontend/src/api/admin.ts Normal file
View File

@ -0,0 +1,52 @@
import { api } from './client'
export interface AIProvider {
id: string; name: string; model: string; endpoint: string
is_active: boolean; has_key: boolean
}
export interface Source { id: string; name: string; type: string; enabled: boolean }
export interface ScrapeJob {
id: string; source_id: string; source_name: string; status: string
started_at: { Time: string; Valid: boolean } | null
finished_at: { Time: string; Valid: boolean } | null
articles_found: number; error_msg: string; created_at: string
}
export interface Setting { key: string; value: string }
export interface AdminUser { id: string; email: string; role: string; created_at: string }
export interface Credential { source_id: string; source_name: string; username: string; has_password: boolean }
export const adminApi = {
// Credentials
getCredentials: () => api.get<Credential[]>('/admin/credentials'),
updateCredentials: (data: { source_id: string; username: string; password?: string }) =>
api.put<void>('/admin/credentials', data),
// AI Providers
listProviders: () => api.get<AIProvider[]>('/admin/ai-providers'),
createProvider: (data: { name: string; api_key?: string; model?: string; endpoint?: string }) =>
api.post<AIProvider>('/admin/ai-providers', data),
updateProvider: (id: string, data: { name: string; api_key?: string; model?: string; endpoint?: string }) =>
api.put<void>(`/admin/ai-providers/${id}`, data),
activateProvider: (id: string) => api.post<void>(`/admin/ai-providers/${id}/activate`),
deleteProvider: (id: string) => api.delete<void>(`/admin/ai-providers/${id}`),
listModels: (id: string) => api.get<string[]>(`/admin/ai-providers/${id}/models`),
// Sources
listSources: () => api.get<Source[]>('/admin/sources'),
updateSource: (id: string, enabled: boolean) => api.put<void>(`/admin/sources/${id}`, { enabled }),
// Jobs
listJobs: () => api.get<ScrapeJob[]>('/admin/scrape-jobs'),
triggerJob: (source_id: string) => api.post<void>('/admin/scrape-jobs/trigger', { source_id }),
// Settings
listSettings: () => api.get<Setting[]>('/admin/settings'),
updateSettings: (settings: Setting[]) => api.put<void>('/admin/settings', { settings }),
getDefaultPrompt: () => api.get<{ prompt: string }>('/admin/settings/default-prompt'),
// Users
listUsers: () => api.get<AdminUser[]>('/admin/users'),
updateUser: (id: string, email: string, role: string) =>
api.put<AdminUser>(`/admin/users/${id}`, { email, role }),
deleteUser: (id: string) => api.delete<void>(`/admin/users/${id}`),
}

View File

@ -0,0 +1,25 @@
import { api } from './client'
export interface Article {
id: string
source_id: string
source_name: string
title: string
content: string
url: string
published_at: { Time: string; Valid: boolean } | null
created_at: string
symbols?: string[]
}
export const articlesApi = {
list: (params?: { symbol?: string; limit?: number; offset?: number }) => {
const q = new URLSearchParams()
if (params?.symbol) q.set('symbol', params.symbol)
if (params?.limit) q.set('limit', String(params.limit))
if (params?.offset) q.set('offset', String(params.offset))
const qs = q.toString()
return api.get<Article[]>(`/articles${qs ? `?${qs}` : ''}`)
},
get: (id: string) => api.get<Article>(`/articles/${id}`),
}

View File

@ -0,0 +1,9 @@
import { api } from './client'
export interface Asset { id: string; user_id: string; symbol: string; name: string; created_at: string }
export const assetsApi = {
list: () => api.get<Asset[]>('/me/assets'),
add: (symbol: string, name: string) => api.post<Asset>('/me/assets', { symbol, name }),
remove: (symbol: string) => api.delete<void>(`/me/assets/${symbol}`),
}

12
frontend/src/api/auth.ts Normal file
View File

@ -0,0 +1,12 @@
import { api } from './client'
export interface User { id: string; email: string; role: 'admin' | 'user' }
interface AuthResponse { token: string; user: User }
export const authApi = {
login: (email: string, password: string) =>
api.post<AuthResponse>('/auth/login', { email, password }),
register: (email: string, password: string) =>
api.post<AuthResponse>('/auth/register', { email, password }),
me: () => api.get<User>('/me'),
}

View File

@ -0,0 +1,33 @@
const BASE = '/api'
function getToken() {
return localStorage.getItem('token')
}
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken()
const res = await fetch(`${BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
},
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || res.statusText)
}
if (res.status === 204) return undefined as T
const json = await res.json()
return json.data !== undefined ? json.data : json
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body?: unknown) =>
request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),
put: <T>(path: string, body?: unknown) =>
request<T>(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
}

View File

@ -0,0 +1,14 @@
import { api } from './client'
export interface Summary {
id: string
user_id: string
content: string
ai_provider_id: string | null
generated_at: string
}
export const summariesApi = {
list: (limit = 10) => api.get<Summary[]>(`/summaries?limit=${limit}`),
generate: () => api.post<Summary>('/summaries/generate'),
}