first commit
Deploy / lint (push) Failing after 7s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-02-20 17:31:01 +01:00
commit 61ab24490d
160 changed files with 17034 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import apiClient from './client';
import type { AdminUser, SystemSetting } from '../lib/types';
// Users
export function listUsers() {
return apiClient.get<AdminUser[]>('admin/users').then((r) => r.data);
}
export function createUser(data: {
username: string;
password: string;
role: string;
has_access: boolean;
}) {
return apiClient.post<AdminUser>('admin/users', data).then((r) => r.data);
}
export function updateAccess(userId: number, hasAccess: boolean) {
return apiClient
.put<{ message: string }>(`admin/users/${userId}/access`, {
has_access: hasAccess,
})
.then((r) => r.data);
}
export function resetPassword(userId: number, password: string) {
return apiClient
.put<{ message: string }>(`admin/users/${userId}/password`, { password })
.then((r) => r.data);
}
// Settings
export function listSettings() {
return apiClient
.get<SystemSetting[]>('admin/settings')
.then((r) => r.data);
}
export function updateSetting(key: string, value: string) {
return apiClient
.put<{ message: string }>(`admin/settings/${key}`, { value })
.then((r) => r.data);
}
export function updateRegistration(enabled: boolean) {
return apiClient
.put<{ message: string }>('admin/settings/registration', { enabled })
.then((r) => r.data);
}
// Jobs
export interface JobStatus {
name: string;
label: string;
enabled: boolean;
next_run_at: string | null;
registered: boolean;
}
export function listJobs() {
return apiClient.get<JobStatus[]>('admin/jobs').then((r) => r.data);
}
export function toggleJob(jobName: string, enabled: boolean) {
return apiClient
.put<{ message: string }>(`admin/jobs/${jobName}/toggle`, { enabled })
.then((r) => r.data);
}
export function triggerJob(jobName: string) {
return apiClient
.post<{ message: string }>(`admin/jobs/${jobName}/trigger`)
.then((r) => r.data);
}
// Data cleanup
export function cleanupData(olderThanDays: number) {
return apiClient
.post<{ message: string }>('admin/data/cleanup', {
older_than_days: olderThanDays,
})
.then((r) => r.data);
}
+14
View File
@@ -0,0 +1,14 @@
import apiClient from './client';
import type { TokenResponse } from '../lib/types';
export function login(username: string, password: string) {
return apiClient
.post<TokenResponse>('auth/login', { username, password })
.then((r) => r.data);
}
export function register(username: string, password: string) {
return apiClient
.post<{ message: string }>('auth/register', { username, password })
.then((r) => r.data);
}
+69
View File
@@ -0,0 +1,69 @@
import axios from 'axios';
import type { APIEnvelope } from '../lib/types';
import { useAuthStore } from '../stores/authStore';
/**
* Typed error class for API errors, providing structured error handling
* across the application.
*/
export class ApiError extends Error {
constructor(message: string) {
super(message);
this.name = 'ApiError';
}
}
/**
* Central Axios instance configured for the Stock Data Backend API.
* - Base URL: /api/v1/
* - Timeout: 30 seconds
* - JSON content type
*/
const apiClient = axios.create({
baseURL: '/api/v1/',
timeout: 30_000,
headers: { 'Content-Type': 'application/json' },
});
/**
* Request interceptor: attaches JWT Bearer token from the auth store
* to every outgoing request when a token is available.
*/
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
/**
* Response interceptor:
* - Success path: unwraps the { status, data, error } envelope, returning
* only the `data` field. Throws ApiError if envelope status is 'error'.
* - Error path: handles 401 by clearing auth and redirecting to login.
* All other errors are wrapped in ApiError with a descriptive message.
*/
apiClient.interceptors.response.use(
(response) => {
const envelope = response.data as APIEnvelope;
if (envelope.status === 'error') {
throw new ApiError(envelope.error ?? 'Unknown API error');
}
// Return unwrapped data — callers receive the inner payload directly.
// We override the response shape here; downstream API functions cast as needed.
response.data = envelope.data;
return response;
},
(error) => {
if (axios.isAxiosError(error) && error.response?.status === 401) {
useAuthStore.getState().logout();
window.location.href = '/login';
}
const msg =
error.response?.data?.error ?? error.message ?? 'Network error';
throw new ApiError(msg);
},
);
export default apiClient;
+8
View File
@@ -0,0 +1,8 @@
import apiClient from './client';
import type { FundamentalResponse } from '../lib/types';
export function getFundamentals(symbol: string) {
return apiClient
.get<FundamentalResponse>(`fundamentals/${symbol}`)
.then((r) => r.data);
}
+5
View File
@@ -0,0 +1,5 @@
import apiClient from './client';
export function check() {
return apiClient.get<{ status: string }>('health').then((r) => r.data);
}
+24
View File
@@ -0,0 +1,24 @@
import apiClient from './client';
import type { IndicatorResult, EMACrossResult } from '../lib/types';
interface IndicatorEnvelopeData {
symbol: string;
indicator: IndicatorResult;
}
interface EMACrossEnvelopeData {
symbol: string;
ema_cross: EMACrossResult;
}
export function getIndicator(symbol: string, indicatorType: string) {
return apiClient
.get<IndicatorEnvelopeData>(`indicators/${symbol}/${indicatorType}`)
.then((r) => (r.data as unknown as IndicatorEnvelopeData).indicator);
}
export function getEMACross(symbol: string) {
return apiClient
.get<EMACrossEnvelopeData>(`indicators/${symbol}/ema-cross`)
.then((r) => (r.data as unknown as EMACrossEnvelopeData).ema_cross);
}
+7
View File
@@ -0,0 +1,7 @@
import apiClient from './client';
export function fetchData(symbol: string) {
return apiClient
.post<{ message: string }>(`ingestion/fetch/${symbol}`)
.then((r) => r.data);
}
+6
View File
@@ -0,0 +1,6 @@
import apiClient from './client';
import type { OHLCVBar } from '../lib/types';
export function getOHLCV(symbol: string) {
return apiClient.get<OHLCVBar[]>(`ohlcv/${symbol}`).then((r) => r.data);
}
+18
View File
@@ -0,0 +1,18 @@
import apiClient from './client';
import type { ScoreResponse, RankingsResponse } from '../lib/types';
export function getScores(symbol: string) {
return apiClient
.get<ScoreResponse>(`scores/${symbol}`)
.then((r) => r.data);
}
export function getRankings() {
return apiClient.get<RankingsResponse>('rankings').then((r) => r.data);
}
export function updateWeights(weights: Record<string, number>) {
return apiClient
.put<{ message: string }>('scores/weights', weights)
.then((r) => r.data);
}
+8
View File
@@ -0,0 +1,8 @@
import apiClient from './client';
import type { SentimentResponse } from '../lib/types';
export function getSentiment(symbol: string) {
return apiClient
.get<SentimentResponse>(`sentiment/${symbol}`)
.then((r) => r.data);
}
+8
View File
@@ -0,0 +1,8 @@
import apiClient from './client';
import type { SRLevelResponse } from '../lib/types';
export function getLevels(symbol: string) {
return apiClient
.get<SRLevelResponse>(`sr-levels/${symbol}`)
.then((r) => r.data);
}
+16
View File
@@ -0,0 +1,16 @@
import apiClient from './client';
import type { Ticker } from '../lib/types';
export function list() {
return apiClient.get<Ticker[]>('tickers').then((r) => r.data);
}
export function create(symbol: string) {
return apiClient.post<Ticker>('tickers', { symbol }).then((r) => r.data);
}
export function deleteTicker(symbol: string) {
return apiClient
.delete<{ message: string }>(`tickers/${symbol}`)
.then((r) => r.data);
}
+6
View File
@@ -0,0 +1,6 @@
import apiClient from './client';
import type { TradeSetup } from '../lib/types';
export function list() {
return apiClient.get<TradeSetup[]>('trades').then((r) => r.data);
}
+18
View File
@@ -0,0 +1,18 @@
import apiClient from './client';
import type { WatchlistEntry } from '../lib/types';
export function list() {
return apiClient.get<WatchlistEntry[]>('watchlist').then((r) => r.data);
}
export function add(symbol: string) {
return apiClient
.post<WatchlistEntry>(`watchlist/${symbol}`)
.then((r) => r.data);
}
export function remove(symbol: string) {
return apiClient
.delete<{ message: string }>(`watchlist/${symbol}`)
.then((r) => r.data);
}