Compare commits

...

4 Commits

Author SHA1 Message Date
shokollm
5eb623f022 feat: setup SvelteKit frontend with TypeScript
Implemented issue #9 - Frontend Project Setup with Svelte and TypeScript.

Changes:
- Created SvelteKit project with TypeScript
- Set up routing structure:
  - / (landing page)
  - /login
  - /register
  - /dashboard
  - /bot/[id]
  - /bot/[id]/backtest
  - /bot/[id]/simulate
  - /settings
- Created Svelte stores for state management:
  - userStore - Current user info
  - botsStore - List of user's bots
  - currentBotStore - Selected bot
  - chatStore - Chat messages
  - backtestStore - Backtest results
  - simulationStore - Simulation signals
  - authStore - Authentication state
- Created API client for backend communication
- Set up environment variables (.env.example)
- Created auth store with protected routes and login/register functionality
2026-04-08 12:58:33 +00:00
0cc3327991 Merge pull request '[Backend] Bot CRUD - Bot Management with Max 3 Limit' (#16) from fix/issue-5 into main 2026-04-08 08:16:19 +02:00
shokollm
429d46c6d0 feat: implement bot CRUD with 3-bot limit per user 2026-04-08 06:05:43 +00:00
a2f0c9a0e9 Merge pull request '[Backend] Auth System - JWT Authentication' (#15) from fix/issue-4 into main 2026-04-08 08:01:24 +02:00
36 changed files with 4698 additions and 35 deletions

View File

@@ -1,57 +1,211 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List, Annotated
from .auth import get_current_user
from ..core.database import get_db from ..core.database import get_db
from ..db.schemas import BotCreate, BotUpdate, BotResponse from ..db.schemas import (
BotCreate,
BotUpdate,
BotResponse,
BotConversationCreate,
BotConversationResponse,
)
from ..db.models import Bot, BotConversation, User
router = APIRouter() router = APIRouter()
MAX_BOTS_PER_USER = 3
@router.get("", response_model=List[BotResponse]) @router.get("", response_model=List[BotResponse])
def list_bots(db: Session = Depends(get_db)): def list_bots(
raise HTTPException( current_user: Annotated[User, Depends(get_current_user)],
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented" db: Session = Depends(get_db),
) ):
bots = db.query(Bot).filter(Bot.user_id == current_user.id).all()
return bots
@router.post("", response_model=BotResponse) @router.post("", response_model=BotResponse, status_code=status.HTTP_201_CREATED)
def create_bot(bot: BotCreate, db: Session = Depends(get_db)): def create_bot(
raise HTTPException( bot_data: BotCreate,
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented" current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
user_bot_count = db.query(Bot).filter(Bot.user_id == current_user.id).count()
if user_bot_count >= MAX_BOTS_PER_USER:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Maximum of {MAX_BOTS_PER_USER} bots per user exceeded",
)
existing_bot = (
db.query(Bot)
.filter(Bot.user_id == current_user.id, Bot.name == bot_data.name)
.first()
) )
if existing_bot:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Bot name must be unique per user",
)
db_bot = Bot(
user_id=current_user.id,
name=bot_data.name,
description=bot_data.description,
strategy_config=bot_data.strategy_config,
llm_config=bot_data.llm_config,
)
db.add(db_bot)
db.commit()
db.refresh(db_bot)
return db_bot
@router.get("/{bot_id}", response_model=BotResponse) @router.get("/{bot_id}", response_model=BotResponse)
def get_bot(bot_id: str, db: Session = Depends(get_db)): def get_bot(
raise HTTPException( bot_id: str,
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented" current_user: Annotated[User, Depends(get_current_user)],
) db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this bot",
)
return bot
@router.put("/{bot_id}", response_model=BotResponse) @router.put("/{bot_id}", response_model=BotResponse)
def update_bot(bot_id: str, bot: BotUpdate, db: Session = Depends(get_db)): def update_bot(
raise HTTPException( bot_id: str,
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented" bot_data: BotUpdate,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this bot",
)
if bot_data.name is not None:
existing_bot = (
db.query(Bot)
.filter(
Bot.user_id == current_user.id,
Bot.name == bot_data.name,
Bot.id != bot_id,
)
.first()
)
if existing_bot:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Bot name must be unique per user",
)
bot.name = bot_data.name
if bot_data.description is not None:
bot.description = bot_data.description
if bot_data.strategy_config is not None:
bot.strategy_config = bot_data.strategy_config
if bot_data.llm_config is not None:
bot.llm_config = bot_data.llm_config
if bot_data.status is not None:
bot.status = bot_data.status
db.commit()
db.refresh(bot)
return bot
@router.delete("/{bot_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_bot(
bot_id: str,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this bot",
)
db.delete(bot)
db.commit()
@router.post("/{bot_id}/chat", response_model=BotConversationResponse)
def chat(
bot_id: str,
message: BotConversationCreate,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to chat with this bot",
)
db_conversation = BotConversation(
bot_id=bot_id,
role=message.role,
content=message.content,
) )
db.add(db_conversation)
db.commit()
db.refresh(db_conversation)
return db_conversation
@router.delete("/{bot_id}") @router.get("/{bot_id}/history", response_model=List[BotConversationResponse])
def delete_bot(bot_id: str, db: Session = Depends(get_db)): def get_history(
raise HTTPException( bot_id: str,
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented" current_user: Annotated[User, Depends(get_current_user)],
) db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
@router.post("/{bot_id}/chat") if not bot:
def chat(bot_id: str, message: dict, db: Session = Depends(get_db)): raise HTTPException(
raise HTTPException( status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented" detail="Bot not found",
) )
if bot.user_id != current_user.id:
raise HTTPException(
@router.get("/{bot_id}/history") status_code=status.HTTP_403_FORBIDDEN,
def get_history(bot_id: str, db: Session = Depends(get_db)): detail="Not authorized to access this bot's history",
raise HTTPException( )
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
conversations = (
db.query(BotConversation)
.filter(BotConversation.bot_id == bot_id)
.order_by(BotConversation.created_at)
.all()
) )
return conversations

View File

@@ -0,0 +1,2 @@
VITE_API_URL=http://localhost:8000/api
VITE_WS_URL=ws://localhost:8000/ws

23
src/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
src/frontend/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

3
src/frontend/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

42
src/frontend/README.md Normal file
View File

@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv@0.15.0 create --template minimal --types ts --no-install .
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

1383
src/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
src/frontend/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "frontend",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.7"
}
}

13
src/frontend/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/frontend/src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,209 @@
import type {
User,
Bot,
BotConversation,
Backtest,
Simulation,
Signal,
AuthResponse,
BotChatRequest,
BotChatResponse,
StrategyConfig
} from './types';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
function getAuthHeaders(): HeadersInit {
const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'An error occurred' }));
throw new Error(error.detail || `HTTP error ${response.status}`);
}
return response.json();
}
export const api = {
auth: {
async register(email: string, password: string): Promise<AuthResponse> {
const response = await fetch(`${API_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
return handleResponse<AuthResponse>(response);
},
async login(email: string, password: string): Promise<AuthResponse> {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
return handleResponse<AuthResponse>(response);
},
async logout(): Promise<void> {
await fetch(`${API_URL}/auth/logout`, {
method: 'POST',
headers: getAuthHeaders()
});
},
async me(): Promise<User> {
const response = await fetch(`${API_URL}/auth/me`, {
headers: getAuthHeaders()
});
return handleResponse<User>(response);
}
},
bots: {
async list(): Promise<Bot[]> {
const response = await fetch(`${API_URL}/bots`, {
headers: getAuthHeaders()
});
return handleResponse<Bot[]>(response);
},
async create(name: string, description?: string): Promise<Bot> {
const response = await fetch(`${API_URL}/bots`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ name, description })
});
return handleResponse<Bot>(response);
},
async get(id: string): Promise<Bot> {
const response = await fetch(`${API_URL}/bots/${id}`, {
headers: getAuthHeaders()
});
return handleResponse<Bot>(response);
},
async update(id: string, data: Partial<Bot>): Promise<Bot> {
const response = await fetch(`${API_URL}/bots/${id}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
return handleResponse<Bot>(response);
},
async delete(id: string): Promise<void> {
const response = await fetch(`${API_URL}/bots/${id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
},
async chat(id: string, message: string): Promise<BotChatResponse> {
const response = await fetch(`${API_URL}/bots/${id}/chat`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ message } as BotChatRequest)
});
return handleResponse<BotChatResponse>(response);
},
async getHistory(id: string): Promise<BotConversation[]> {
const response = await fetch(`${API_URL}/bots/${id}/history`, {
headers: getAuthHeaders()
});
return handleResponse<BotConversation[]>(response);
}
},
backtest: {
async start(botId: string, config: { token: string; timeframe: string; start_date: string; end_date: string }): Promise<Backtest> {
const response = await fetch(`${API_URL}/bots/${botId}/backtest`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config)
});
return handleResponse<Backtest>(response);
},
async get(botId: string, runId: string): Promise<Backtest> {
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}`, {
headers: getAuthHeaders()
});
return handleResponse<Backtest>(response);
},
async list(botId: string): Promise<Backtest[]> {
const response = await fetch(`${API_URL}/bots/${botId}/backtests`, {
headers: getAuthHeaders()
});
return handleResponse<Backtest[]>(response);
},
async stop(botId: string, runId: string): Promise<void> {
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}/stop`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
}
},
simulate: {
async start(botId: string, config: { token: string; interval_seconds: number; auto_execute: boolean }): Promise<Simulation> {
const response = await fetch(`${API_URL}/bots/${botId}/simulate`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config)
});
return handleResponse<Simulation>(response);
},
async get(botId: string, runId: string): Promise<Simulation> {
const response = await fetch(`${API_URL}/bots/${botId}/simulate/${runId}`, {
headers: getAuthHeaders()
});
return handleResponse<Simulation>(response);
},
async list(botId: string): Promise<Simulation[]> {
const response = await fetch(`${API_URL}/bots/${botId}/simulations`, {
headers: getAuthHeaders()
});
return handleResponse<Simulation[]>(response);
},
async stop(botId: string, runId: string): Promise<void> {
const response = await fetch(`${API_URL}/bots/${botId}/simulate/${runId}/stop`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
}
},
config: {
async getChains(): Promise<string[]> {
const response = await fetch(`${API_URL}/config/chains`, {
headers: getAuthHeaders()
});
return handleResponse<string[]>(response);
},
async getTokens(): Promise<{ symbol: string; chain: string; name: string }[]> {
const response = await fetch(`${API_URL}/config/tokens`, {
headers: getAuthHeaders()
});
return handleResponse<{ symbol: string; chain: string; name: string }[]>(response);
}
}
};

View File

@@ -0,0 +1,2 @@
export { api } from './client';
export * from './types';

View File

@@ -0,0 +1,128 @@
export interface User {
id: string;
email: string;
created_at: string;
updated_at: string;
}
export interface Bot {
id: string;
user_id: string;
name: string;
description: string | null;
strategy_config: StrategyConfig;
llm_config: LLMConfig;
status: 'draft' | 'active' | 'paused';
created_at: string;
updated_at: string;
}
export interface StrategyConfig {
conditions: Condition[];
actions: Action[];
risk_management?: RiskManagement;
}
export interface Condition {
type: 'price_drop' | 'price_rise' | 'volume_spike' | 'price_level';
token: string;
chain?: string;
threshold?: number;
price?: number;
direction?: 'above' | 'below';
timeframe?: string;
}
export interface Action {
type: 'buy' | 'sell' | 'hold';
amount_percent?: number;
token?: string;
}
export interface RiskManagement {
stop_loss_percent?: number;
take_profit_percent?: number;
}
export interface LLMConfig {
model: string;
temperature: number;
}
export interface BotConversation {
id: string;
bot_id: string;
role: 'user' | 'assistant' | 'system';
content: string;
created_at: string;
}
export interface Backtest {
id: string;
bot_id: string;
started_at: string;
ended_at: string | null;
status: 'running' | 'completed' | 'failed';
config: BacktestConfig;
result: BacktestResult | null;
}
export interface BacktestConfig {
token: string;
timeframe: string;
start_date: string;
end_date: string;
}
export interface BacktestResult {
total_return: number;
win_rate: number;
total_trades: number;
buy_signals: number;
sell_signals: number;
max_drawdown: number;
sharpe_ratio: number;
}
export interface Simulation {
id: string;
bot_id: string;
started_at: string;
status: 'running' | 'stopped';
config: SimulationConfig;
signals: Signal[] | null;
}
export interface SimulationConfig {
token: string;
interval_seconds: number;
auto_execute: boolean;
}
export interface Signal {
id: string;
bot_id: string;
run_id: string;
signal_type: 'buy' | 'sell' | 'hold';
token: string;
price: number;
confidence: number | null;
reasoning: string | null;
executed: boolean;
created_at: string;
}
export interface AuthResponse {
access_token: string;
token_type: string;
}
export interface BotChatRequest {
message: string;
}
export interface BotChatResponse {
response: string;
strategy_config: StrategyConfig | null;
success: boolean;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,50 @@
import { writable, get } from 'svelte/store';
import { api } from '$lib/api';
import { setUser, clearUser, clearBots } from './index';
import { clearSimulationState } from './simulationStore';
import { clearBacktestState } from './backtestStore';
export const isAuthenticated = writable(false);
export const isLoading = writable(true);
export async function initAuth() {
isLoading.set(true);
const token = localStorage.getItem('token');
if (token) {
try {
const user = await api.auth.me();
setUser(user);
isAuthenticated.set(true);
} catch {
localStorage.removeItem('token');
isAuthenticated.set(false);
}
}
isLoading.set(false);
}
export async function login(email: string, password: string) {
const response = await api.auth.login(email, password);
localStorage.setItem('token', response.access_token);
const user = await api.auth.me();
setUser(user);
isAuthenticated.set(true);
}
export async function register(email: string, password: string) {
const response = await api.auth.register(email, password);
localStorage.setItem('token', response.access_token);
const user = await api.auth.me();
setUser(user);
isAuthenticated.set(true);
}
export function logout() {
api.auth.logout().catch(() => {});
localStorage.removeItem('token');
clearUser();
clearBots();
clearBacktestState();
clearSimulationState();
isAuthenticated.set(false);
}

View File

@@ -0,0 +1,45 @@
import { writable } from 'svelte/store';
import type { Backtest, BacktestResult } from '$lib/api';
export interface BacktestState {
currentBacktest: Backtest | null;
backtestHistory: Backtest[];
isLoading: boolean;
error: string | null;
}
const initialState: BacktestState = {
currentBacktest: null,
backtestHistory: [],
isLoading: false,
error: null
};
export const backtestStore = writable<BacktestState>(initialState);
export function setCurrentBacktest(backtest: Backtest | null) {
backtestStore.update(state => ({ ...state, currentBacktest: backtest }));
}
export function addBacktestToHistory(backtest: Backtest) {
backtestStore.update(state => ({
...state,
backtestHistory: [backtest, ...state.backtestHistory]
}));
}
export function setBacktestHistory(backtests: Backtest[]) {
backtestStore.update(state => ({ ...state, backtestHistory: backtests }));
}
export function setBacktestLoading(loading: boolean) {
backtestStore.update(state => ({ ...state, isLoading: loading }));
}
export function setBacktestError(error: string | null) {
backtestStore.update(state => ({ ...state, error }));
}
export function clearBacktestState() {
backtestStore.set(initialState);
}

View File

@@ -0,0 +1,24 @@
import { writable } from 'svelte/store';
import type { Bot } from '$lib/api';
export const botsStore = writable<Bot[]>([]);
export function setBots(bots: Bot[]) {
botsStore.set(bots);
}
export function addBot(bot: Bot) {
botsStore.update(bots => [...bots, bot]);
}
export function updateBot(bot: Bot) {
botsStore.update(bots => bots.map(b => b.id === bot.id ? bot : b));
}
export function removeBot(botId: string) {
botsStore.update(bots => bots.filter(b => b.id !== botId));
}
export function clearBots() {
botsStore.set([]);
}

View File

@@ -0,0 +1,33 @@
import { writable } from 'svelte/store';
import type { BotConversation } from '$lib/api';
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
}
export const chatStore = writable<ChatMessage[]>([]);
export function addMessage(message: Omit<ChatMessage, 'id' | 'timestamp'>) {
const newMessage: ChatMessage = {
...message,
id: crypto.randomUUID(),
timestamp: new Date()
};
chatStore.update(messages => [...messages, newMessage]);
}
export function setMessages(messages: BotConversation[]) {
chatStore.set(messages.map(m => ({
id: m.id,
role: m.role,
content: m.content,
timestamp: new Date(m.created_at)
})));
}
export function clearChat() {
chatStore.set([]);
}

View File

@@ -0,0 +1,12 @@
import { writable } from 'svelte/store';
import type { Bot } from '$lib/api';
export const currentBotStore = writable<Bot | null>(null);
export function setCurrentBot(bot: Bot | null) {
currentBotStore.set(bot);
}
export function clearCurrentBot() {
currentBotStore.set(null);
}

View File

@@ -0,0 +1,30 @@
export { userStore, setUser, clearUser } from './userStore';
export { botsStore, setBots, addBot, updateBot, removeBot, clearBots } from './botsStore';
export { currentBotStore, setCurrentBot, clearCurrentBot } from './currentBotStore';
export { chatStore, addMessage, setMessages, clearChat } from './chatStore';
export {
backtestStore,
setCurrentBacktest,
addBacktestToHistory,
setBacktestHistory,
setBacktestLoading,
setBacktestError,
clearBacktestState
} from './backtestStore';
export {
simulationStore,
setCurrentSimulation,
addSignals,
clearSignals,
setSimulationLoading,
setSimulationError,
clearSimulationState
} from './simulationStore';
export {
isAuthenticated,
isLoading,
initAuth,
login,
register,
logout
} from './authStore';

View File

@@ -0,0 +1,45 @@
import { writable } from 'svelte/store';
import type { Simulation, Signal } from '$lib/api';
export interface SimulationState {
currentSimulation: Simulation | null;
signals: Signal[];
isLoading: boolean;
error: string | null;
}
const initialState: SimulationState = {
currentSimulation: null,
signals: [],
isLoading: false,
error: null
};
export const simulationStore = writable<SimulationState>(initialState);
export function setCurrentSimulation(simulation: Simulation | null) {
simulationStore.update(state => ({ ...state, currentSimulation: simulation }));
}
export function addSignals(newSignals: Signal[]) {
simulationStore.update(state => ({
...state,
signals: [...state.signals, ...newSignals]
}));
}
export function clearSignals() {
simulationStore.update(state => ({ ...state, signals: [] }));
}
export function setSimulationLoading(loading: boolean) {
simulationStore.update(state => ({ ...state, isLoading: loading }));
}
export function setSimulationError(error: string | null) {
simulationStore.update(state => ({ ...state, error }));
}
export function clearSimulationState() {
simulationStore.set(initialState);
}

View File

@@ -0,0 +1,12 @@
import { writable } from 'svelte/store';
import type { User } from '$lib/api';
export const userStore = writable<User | null>(null);
export function setUser(user: User | null) {
userStore.set(user);
}
export function clearUser() {
userStore.set(null);
}

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { onMount } from 'svelte';
import { initAuth, isLoading } from '$lib/stores';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
onMount(() => {
initAuth();
});
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{#if $isLoading}
<div class="loading">
<div class="spinner"></div>
</div>
{:else}
{@render children()}
{/if}
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
.loading {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,135 @@
<script lang="ts">
import { isAuthenticated } from '$lib/stores';
</script>
<svelte:head>
<title>Randebu - AI Trading Bot Platform</title>
</svelte:head>
{#if $isAuthenticated}
<script>
window.location.href = '/dashboard';
</script>
{:else}
<main>
<div class="hero">
<h1>Randebu</h1>
<p class="tagline">Create trading bots through conversation with AI</p>
<div class="cta">
<a href="/register" class="btn btn-primary">Get Started</a>
<a href="/login" class="btn btn-secondary">Login</a>
</div>
</div>
<section class="features">
<h2>How It Works</h2>
<div class="feature-grid">
<div class="feature">
<h3>1. Describe Your Strategy</h3>
<p>Tell our AI what kind of trading you want to do in plain English</p>
</div>
<div class="feature">
<h3>2. Backtest & Validate</h3>
<p>Test your strategy against historical data before risking real funds</p>
</div>
<div class="feature">
<h3>3. Simulate & Monitor</h3>
<p>Run real-time simulations and watch for trading signals</p>
</div>
</div>
</section>
</main>
{/if}
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
.hero {
text-align: center;
max-width: 600px;
}
h1 {
font-size: 3.5rem;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.tagline {
font-size: 1.25rem;
color: #aaa;
margin: 1rem 0 2rem;
}
.cta {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: transform 0.2s, opacity 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.features {
margin-top: 4rem;
text-align: center;
}
.features h2 {
font-size: 2rem;
margin-bottom: 2rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
max-width: 800px;
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.feature p {
color: #888;
font-size: 0.9rem;
}
</style>

View File

@@ -0,0 +1,354 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, chatStore, addMessage, setMessages, clearChat, currentBotStore, setCurrentBot } from '$lib/stores';
import { api } from '$lib/api';
let botId = $derived($page.params.id);
let messageInput = $state('');
let isSending = $state(false);
let chatContainer: HTMLDivElement;
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated && botId) {
await loadBot();
await loadChatHistory();
}
});
async function loadBot() {
try {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
} catch (e) {
goto('/dashboard');
}
}
async function loadChatHistory() {
try {
const history = await api.bots.getHistory(botId);
setMessages(history);
scrollToBottom();
} catch (e) {
console.error('Failed to load chat history:', e);
}
}
async function sendMessage() {
if (!messageInput.trim() || isSending) return;
const userMessage = messageInput;
messageInput = '';
isSending = true;
addMessage({ role: 'user', content: userMessage });
scrollToBottom();
try {
const response = await api.bots.chat(botId, userMessage);
addMessage({ role: 'assistant', content: response.response });
if (response.strategy_config) {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
}
} catch (e) {
addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' });
} finally {
isSending = false;
scrollToBottom();
}
}
function scrollToBottom() {
setTimeout(() => {
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 50);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
</script>
<svelte:head>
<title>{$currentBotStore?.name || 'Bot'} - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/dashboard" class="back-link">← Dashboard</a>
<h1>{$currentBotStore?.name || 'Loading...'}</h1>
</div>
<div class="header-actions">
<a href="/bot/{botId}/backtest" class="btn btn-secondary">Backtest</a>
<a href="/bot/{botId}/simulate" class="btn btn-secondary">Simulate</a>
</div>
</header>
<div class="chat-container" bind:this={chatContainer}>
{#if $chatStore.length === 0}
<div class="welcome-message">
<p>Welcome to {$currentBotStore?.name}! Describe your trading strategy in plain English.</p>
<p class="hint">Example: "Buy PEPE when the price drops by 5% within 1 hour"</p>
</div>
{/if}
{#each $chatStore as message}
<div class="message {message.role}">
<div class="message-content">
{message.content}
</div>
<div class="message-time">
{message.timestamp.toLocaleTimeString()}
</div>
</div>
{/each}
{#if isSending}
<div class="message assistant">
<div class="message-content typing">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
{/if}
</div>
{#if $currentBotStore}
<div class="input-container">
<textarea
bind:value={messageInput}
onkeydown={handleKeydown}
placeholder="Describe your trading strategy..."
rows="1"
disabled={isSending}
></textarea>
<button onclick={sendMessage} disabled={isSending || !messageInput.trim()}>
Send
</button>
</div>
{/if}
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
height: 100vh;
display: flex;
flex-direction: column;
max-width: 900px;
margin: 0 auto;
padding: 1rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 1rem;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
.back-link:hover {
text-decoration: underline;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
border: none;
transition: transform 0.2s;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn:hover {
transform: translateY(-2px);
}
.chat-container {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.welcome-message {
text-align: center;
padding: 2rem;
color: #888;
}
.welcome-message .hint {
font-size: 0.85rem;
margin-top: 1rem;
color: #666;
}
.message {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.message.user {
align-items: flex-end;
}
.message.assistant {
align-items: flex-start;
}
.message-content {
max-width: 70%;
padding: 0.75rem 1rem;
border-radius: 12px;
line-height: 1.5;
}
.message.user .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: rgba(255, 255, 255, 0.1);
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 0.7rem;
color: #666;
margin-top: 0.25rem;
padding: 0 0.5rem;
}
.typing {
display: flex;
gap: 4px;
padding: 1rem 1.25rem;
}
.dot {
width: 8px;
height: 8px;
background: #888;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.4;
}
30% {
transform: translateY(-4px);
opacity: 1;
}
}
.input-container {
display: flex;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
textarea {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
resize: none;
font-family: inherit;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
button {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,391 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, backtestStore, setCurrentBacktest, addBacktestToHistory, setBacktestLoading, setBacktestError } from '$lib/stores';
import { api } from '$lib/api';
let botId = $derived($page.params.id);
let token = $state('PEPE');
let timeframe = $state('1h');
let startDate = $state('');
let endDate = $state('');
let isRunning = $state(false);
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated && botId) {
await loadBot();
await loadBacktests();
}
});
async function loadBot() {
try {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
} catch (e) {
goto('/dashboard');
}
}
async function loadBacktests() {
try {
const backtests = await api.backtest.list(botId);
setBacktestHistory(backtests);
} catch (e) {
console.error('Failed to load backtests:', e);
}
}
async function startBacktest() {
if (!startDate || !endDate) return;
setBacktestError(null);
setBacktestLoading(true);
isRunning = true;
try {
const backtest = await api.backtest.start(botId, {
token,
timeframe,
start_date: startDate,
end_date: endDate
});
setCurrentBacktest(backtest);
addBacktestToHistory(backtest);
} catch (e) {
setBacktestError(e instanceof Error ? e.message : 'Failed to start backtest');
} finally {
setBacktestLoading(false);
isRunning = false;
}
}
async function stopBacktest(runId: string) {
try {
await api.backtest.stop(botId, runId);
await loadBacktests();
} catch (e) {
console.error('Failed to stop backtest:', e);
}
}
function setBacktestHistory(backtests: any[]) {
backtestStore.update(state => ({ ...state, backtestHistory: backtests }));
}
</script>
<svelte:head>
<title>Backtest - {$currentBotStore?.name || 'Bot'} - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/bot/{botId}" class="back-link">← Back to Chat</a>
<h1>Backtest</h1>
</div>
</header>
<div class="content">
<section class="config-section">
<h2>Configure Backtest</h2>
{#if $backtestStore.error}
<div class="error">{$backtestStore.error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); startBacktest(); }}>
<div class="form-row">
<div class="field">
<label for="token">Token</label>
<input type="text" id="token" bind:value={token} required />
</div>
<div class="field">
<label for="timeframe">Timeframe</label>
<select id="timeframe" bind:value={timeframe}>
<option value="1m">1 minute</option>
<option value="5m">5 minutes</option>
<option value="15m">15 minutes</option>
<option value="1h">1 hour</option>
<option value="4h">4 hours</option>
<option value="1d">1 day</option>
</select>
</div>
</div>
<div class="form-row">
<div class="field">
<label for="startDate">Start Date</label>
<input type="date" id="startDate" bind:value={startDate} required />
</div>
<div class="field">
<label for="endDate">End Date</label>
<input type="date" id="endDate" bind:value={endDate} required />
</div>
</div>
<button type="submit" disabled={isRunning || $backtestStore.isLoading}>
{isRunning ? 'Running...' : 'Start Backtest'}
</button>
</form>
</section>
<section class="results-section">
<h2>Backtest History</h2>
{#if $backtestStore.backtestHistory.length === 0}
<p class="empty-state">No backtests yet. Run your first backtest above.</p>
{:else}
<div class="backtest-list">
{#each $backtestStore.backtestHistory as backtest}
<div class="backtest-card">
<div class="backtest-header">
<span class="backtest-status status-{backtest.status}">{backtest.status}</span>
<span class="backtest-date">{new Date(backtest.started_at).toLocaleDateString()}</span>
</div>
{#if backtest.result}
<div class="backtest-results">
<div class="result-item">
<span class="result-label">Total Return</span>
<span class="result-value" class:positive={backtest.result.total_return > 0} class:negative={backtest.result.total_return < 0}>
{backtest.result.total_return.toFixed(2)}%
</span>
</div>
<div class="result-item">
<span class="result-label">Win Rate</span>
<span class="result-value">{backtest.result.win_rate.toFixed(1)}%</span>
</div>
<div class="result-item">
<span class="result-label">Total Trades</span>
<span class="result-value">{backtest.result.total_trades}</span>
</div>
<div class="result-item">
<span class="result-label">Max Drawdown</span>
<span class="result-value negative">{backtest.result.max_drawdown.toFixed(2)}%</span>
</div>
</div>
{/if}
{#if backtest.status === 'running'}
<button onclick={() => stopBacktest(backtest.id)} class="btn btn-danger">Stop</button>
{/if}
</div>
{/each}
</div>
{/if}
</section>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
header {
margin-bottom: 2rem;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
h1 {
margin: 0;
font-size: 1.75rem;
}
h2 {
font-size: 1.25rem;
margin: 0 0 1rem;
}
.content {
display: grid;
gap: 2rem;
}
section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-size: 0.9rem;
color: #ccc;
}
input, select {
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.empty-state {
color: #888;
text-align: center;
padding: 2rem;
}
.backtest-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.backtest-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 1rem;
}
.backtest-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.backtest-status {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.status-running {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
}
.status-completed {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-failed {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.backtest-date {
color: #888;
font-size: 0.85rem;
}
.backtest-results {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.result-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.result-label {
font-size: 0.75rem;
color: #888;
}
.result-value {
font-size: 1.1rem;
font-weight: 500;
}
.positive {
color: #22c55e;
}
.negative {
color: #ef4444;
}
.btn-danger {
margin-top: 0.75rem;
width: auto;
padding: 0.5rem 1rem;
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
</style>

View File

@@ -0,0 +1,426 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
import { api } from '$lib/api';
let botId = $derived($page.params.id);
let token = $state('PEPE');
let intervalSeconds = $state(60);
let autoExecute = $state(false);
let isRunning = $state(false);
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated && botId) {
await loadBot();
await loadSimulations();
}
});
async function loadBot() {
try {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
} catch (e) {
goto('/dashboard');
}
}
async function loadSimulations() {
try {
const simulations = await api.simulate.list(botId);
if (simulations.length > 0) {
const latest = simulations[0];
setCurrentSimulation(latest);
if (latest.signals) {
addSignals(latest.signals);
}
if (latest.status === 'running') {
isRunning = true;
}
}
} catch (e) {
console.error('Failed to load simulations:', e);
}
}
async function startSimulation() {
setSimulationError(null);
setSimulationLoading(true);
isRunning = true;
try {
const simulation = await api.simulate.start(botId, {
token,
interval_seconds: intervalSeconds,
auto_execute: autoExecute
});
setCurrentSimulation(simulation);
clearSignals();
} catch (e) {
setSimulationError(e instanceof Error ? e.message : 'Failed to start simulation');
isRunning = false;
} finally {
setSimulationLoading(false);
}
}
async function stopSimulation() {
if (!$simulationStore.currentSimulation) return;
try {
await api.simulate.stop(botId, $simulationStore.currentSimulation.id);
await loadSimulations();
isRunning = false;
} catch (e) {
console.error('Failed to stop simulation:', e);
}
}
</script>
<svelte:head>
<title>Simulate - {$currentBotStore?.name || 'Bot'} - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/bot/{botId}" class="back-link">← Back to Chat</a>
<h1>Simulation</h1>
</div>
</header>
<div class="notice">
<span class="notice-icon">⚠️</span>
<span>Simulation Mode - Using REST polling (every {intervalSeconds}s). For real-time signals, consider upgrading to Pro tier.</span>
</div>
<div class="content">
<section class="config-section">
<h2>Configure Simulation</h2>
{#if $simulationStore.error}
<div class="error">{$simulationStore.error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); startSimulation(); }}>
<div class="form-row">
<div class="field">
<label for="token">Token</label>
<input type="text" id="token" bind:value={token} required disabled={isRunning} />
</div>
<div class="field">
<label for="interval">Check Interval (seconds)</label>
<select id="interval" bind:value={intervalSeconds} disabled={isRunning}>
<option value={30}>30 seconds</option>
<option value={60}>60 seconds</option>
<option value={120}>2 minutes</option>
<option value={300}>5 minutes</option>
</select>
</div>
</div>
<div class="field checkbox-field">
<input type="checkbox" id="autoExecute" bind:checked={autoExecute} disabled={isRunning} />
<label for="autoExecute">Auto-execute trades (requires Pro tier)</label>
</div>
{#if isRunning}
<button type="button" onclick={stopSimulation} class="btn btn-danger">
Stop Simulation
</button>
{:else}
<button type="submit" disabled={$simulationStore.isLoading}>
{$simulationStore.isLoading ? 'Starting...' : 'Start Simulation'}
</button>
{/if}
</form>
</section>
<section class="signals-section">
<h2>Signals ({$simulationStore.signals.length})</h2>
{#if $simulationStore.signals.length === 0}
<p class="empty-state">No signals yet. Start a simulation to see trading signals.</p>
{:else}
<div class="signals-list">
{#each $simulationStore.signals as signal}
<div class="signal-card">
<div class="signal-header">
<span class="signal-type type-{signal.signal_type}">{signal.signal_type}</span>
<span class="signal-token">{signal.token}</span>
<span class="signal-price">${signal.price.toFixed(6)}</span>
</div>
{#if signal.confidence}
<div class="signal-confidence">
<span>Confidence: {(signal.confidence * 100).toFixed(1)}%</span>
<div class="confidence-bar">
<div class="confidence-fill" style="width: {signal.confidence * 100}%"></div>
</div>
</div>
{/if}
{#if signal.reasoning}
<p class="signal-reasoning">{signal.reasoning}</p>
{/if}
<div class="signal-time">
{new Date(signal.created_at).toLocaleString()}
</div>
</div>
{/each}
</div>
{/if}
</section>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
header {
margin-bottom: 1.5rem;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
h1 {
margin: 0;
font-size: 1.75rem;
}
.notice {
display: flex;
align-items: center;
gap: 0.75rem;
background: rgba(251, 191, 36, 0.1);
border: 1px solid rgba(251, 191, 36, 0.3);
border-radius: 8px;
padding: 0.75rem 1rem;
margin-bottom: 1.5rem;
font-size: 0.9rem;
color: #fbbf24;
}
.notice-icon {
font-size: 1.25rem;
}
.content {
display: grid;
gap: 2rem;
}
section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
h2 {
font-size: 1.25rem;
margin: 0 0 1rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.checkbox-field {
flex-direction: row;
align-items: center;
margin-bottom: 1rem;
}
.checkbox-field input {
width: auto;
margin-right: 0.5rem;
}
.checkbox-field label {
margin: 0;
}
label {
font-size: 0.9rem;
color: #ccc;
}
input, select {
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-danger {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.empty-state {
color: #888;
text-align: center;
padding: 2rem;
}
.signals-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.signal-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 1rem;
}
.signal-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.signal-type {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.type-buy {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.type-sell {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.type-hold {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.signal-token {
font-weight: 500;
}
.signal-price {
color: #888;
font-size: 0.9rem;
}
.signal-confidence {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: #888;
}
.confidence-bar {
flex: 1;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
}
.signal-reasoning {
font-size: 0.9rem;
color: #ccc;
margin: 0.5rem 0;
line-height: 1.4;
}
.signal-time {
font-size: 0.75rem;
color: #666;
}
</style>

View File

@@ -0,0 +1,359 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, botsStore, setBots, addBot, removeBot, userStore, logout } from '$lib/stores';
import { api } from '$lib/api';
let showCreateModal = $state(false);
let newBotName = $state('');
let newBotDescription = $state('');
let isCreating = $state(false);
let createError = $state('');
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated) {
await loadBots();
}
});
async function loadBots() {
try {
const bots = await api.bots.list();
setBots(bots);
} catch (e) {
console.error('Failed to load bots:', e);
}
}
async function createBot() {
if (!$newBotName.trim()) return;
createError = '';
isCreating = true;
try {
const bot = await api.bots.create(newBotName, newBotDescription);
addBot(bot);
showCreateModal = false;
newBotName = '';
newBotDescription = '';
goto(`/bot/${bot.id}`);
} catch (e) {
createError = e instanceof Error ? e.message : 'Failed to create bot';
} finally {
isCreating = false;
}
}
async function deleteBot(botId: string) {
if (!confirm('Are you sure you want to delete this bot?')) return;
try {
await api.bots.delete(botId);
removeBot(botId);
} catch (e) {
console.error('Failed to delete bot:', e);
}
}
function handleLogout() {
logout();
goto('/');
}
</script>
<svelte:head>
<title>Dashboard - Randebu</title>
</svelte:head>
<main>
<header>
<h1>Dashboard</h1>
<div class="header-actions">
<span class="user-email">{$userStore?.email}</span>
<a href="/settings" class="btn btn-secondary">Settings</a>
<button onclick={handleLogout} class="btn btn-secondary">Logout</button>
</div>
</header>
<section class="bots-section">
<div class="section-header">
<h2>Your Bots ({$botsStore.length}/3)</h2>
{#if $botsStore.length < 3}
<button onclick={() => showCreateModal = true} class="btn btn-primary">
Create New Bot
</button>
{/if}
</div>
{#if $botsStore.length === 0}
<div class="empty-state">
<p>You haven't created any bots yet.</p>
<p>Create your first bot to start trading!</p>
</div>
{:else}
<div class="bots-grid">
{#each $botsStore as bot}
<div class="bot-card">
<div class="bot-info">
<h3>{bot.name}</h3>
{#if bot.description}
<p class="bot-description">{bot.description}</p>
{/if}
<span class="bot-status status-{bot.status}">{bot.status}</span>
</div>
<div class="bot-actions">
<a href="/bot/{bot.id}" class="btn btn-primary">Open</a>
<button onclick={() => deleteBot(bot.id)} class="btn btn-danger">Delete</button>
</div>
</div>
{/each}
</div>
{/if}
</section>
{#if showCreateModal}
<div class="modal-overlay" onclick={() => showCreateModal = false}>
<div class="modal" onclick={(e) => e.stopPropagation()}>
<h2>Create New Bot</h2>
{#if createError}
<div class="error">{createError}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); createBot(); }}>
<div class="field">
<label for="botName">Bot Name</label>
<input type="text" id="botName" bind:value={newBotName} required />
</div>
<div class="field">
<label for="botDescription">Description (optional)</label>
<textarea id="botDescription" bind:value={newBotDescription} rows="3"></textarea>
</div>
<div class="modal-actions">
<button type="button" onclick={() => showCreateModal = false} class="btn btn-secondary">
Cancel
</button>
<button type="submit" disabled={isCreating}>
{isCreating ? 'Creating...' : 'Create'}
</button>
</div>
</form>
</div>
</div>
{/if}
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
margin: 0;
font-size: 2rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.user-email {
color: #888;
font-size: 0.9rem;
}
h2 {
margin: 0;
font-size: 1.25rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.bots-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.bot-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.bot-info {
margin-bottom: 1rem;
}
.bot-info h3 {
margin: 0 0 0.5rem;
font-size: 1.25rem;
}
.bot-description {
color: #888;
font-size: 0.9rem;
margin: 0 0 0.75rem;
}
.bot-status {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.status-draft {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.status-active {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-paused {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.bot-actions {
display: flex;
gap: 0.75rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
border: none;
transition: transform 0.2s, opacity 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-danger {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.btn:hover {
transform: translateY(-2px);
}
.empty-state {
text-align: center;
padding: 3rem;
color: #888;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
background: #1a1a1a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2rem;
width: 100%;
max-width: 450px;
}
.modal h2 {
margin: 0 0 1.5rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input, textarea {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
</style>

View File

@@ -0,0 +1,176 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { login, isAuthenticated } from '$lib/stores';
let email = $state('');
let password = $state('');
let error = $state('');
let isLoading = $state(false);
$effect(() => {
if ($isAuthenticated) {
goto('/dashboard');
}
});
async function handleSubmit() {
error = '';
isLoading = true;
try {
await login(email, password);
goto('/dashboard');
} catch (e) {
error = e instanceof Error ? e.message : 'Login failed';
} finally {
isLoading = false;
}
}
</script>
<svelte:head>
<title>Login - Randebu</title>
</svelte:head>
<main>
<div class="auth-card">
<h1>Login</h1>
<p class="subtitle">Welcome back</p>
{#if error}
<div class="error">{error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" bind:value={password} required />
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
<p class="footer">
Don't have an account? <a href="/register">Register</a>
</p>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.auth-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 400px;
}
h1 {
margin: 0;
font-size: 1.75rem;
text-align: center;
}
.subtitle {
color: #888;
text-align: center;
margin: 0.5rem 0 2rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1.25rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.footer {
text-align: center;
margin-top: 1.5rem;
color: #888;
font-size: 0.9rem;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,193 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { register, isAuthenticated } from '$lib/stores';
let email = $state('');
let password = $state('');
let confirmPassword = $state('');
let error = $state('');
let isLoading = $state(false);
$effect(() => {
if ($isAuthenticated) {
goto('/dashboard');
}
});
async function handleSubmit() {
error = '';
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 6) {
error = 'Password must be at least 6 characters';
return;
}
isLoading = true;
try {
await register(email, password);
goto('/dashboard');
} catch (e) {
error = e instanceof Error ? e.message : 'Registration failed';
} finally {
isLoading = false;
}
}
</script>
<svelte:head>
<title>Register - Randebu</title>
</svelte:head>
<main>
<div class="auth-card">
<h1>Create Account</h1>
<p class="subtitle">Start creating trading bots</p>
{#if error}
<div class="error">{error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" bind:value={password} required minlength="6" />
</div>
<div class="field">
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" bind:value={confirmPassword} required />
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating account...' : 'Create Account'}
</button>
</form>
<p class="footer">
Already have an account? <a href="/login">Login</a>
</p>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.auth-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 400px;
}
h1 {
margin: 0;
font-size: 1.75rem;
text-align: center;
}
.subtitle {
color: #888;
text-align: center;
margin: 0.5rem 0 2rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1.25rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.footer {
text-align: center;
margin-top: 1.5rem;
color: #888;
font-size: 0.9rem;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,281 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, userStore, logout } from '$lib/stores';
import { api } from '$lib/api';
let email = $state('');
let currentPassword = $state('');
let newPassword = $state('');
let confirmPassword = $state('');
let isUpdating = $state(false);
let updateSuccess = $state('');
let updateError = $state('');
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($userStore) {
email = $userStore.email;
}
});
async function updateEmail() {
updateSuccess = '';
updateError = '';
isUpdating = true;
try {
await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000/api'}/auth/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ email })
});
updateSuccess = 'Email updated successfully';
} catch (e) {
updateError = e instanceof Error ? e.message : 'Failed to update email';
} finally {
isUpdating = false;
}
}
async function updatePassword() {
updateSuccess = '';
updateError = '';
if (newPassword !== confirmPassword) {
updateError = 'Passwords do not match';
return;
}
if (newPassword.length < 6) {
updateError = 'Password must be at least 6 characters';
return;
}
isUpdating = true;
try {
await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000/api'}/auth/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ password: newPassword, current_password: currentPassword })
});
updateSuccess = 'Password updated successfully';
currentPassword = '';
newPassword = '';
confirmPassword = '';
} catch (e) {
updateError = e instanceof Error ? e.message : 'Failed to update password';
} finally {
isUpdating = false;
}
}
function handleLogout() {
logout();
goto('/');
}
</script>
<svelte:head>
<title>Settings - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/dashboard" class="back-link">← Dashboard</a>
<h1>Settings</h1>
</div>
</header>
<div class="content">
<section class="settings-section">
<h2>Profile</h2>
{#if updateSuccess}
<div class="success">{updateSuccess}</div>
{/if}
{#if updateError}
<div class="error">{updateError}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); updateEmail(); }}>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<button type="submit" disabled={isUpdating}>
{isUpdating ? 'Updating...' : 'Update Email'}
</button>
</form>
</section>
<section class="settings-section">
<h2>Change Password</h2>
<form onsubmit={(e) => { e.preventDefault(); updatePassword(); }}>
<div class="field">
<label for="currentPassword">Current Password</label>
<input type="password" id="currentPassword" bind:value={currentPassword} required />
</div>
<div class="field">
<label for="newPassword">New Password</label>
<input type="password" id="newPassword" bind:value={newPassword} required minlength="6" />
</div>
<div class="field">
<label for="confirmPassword">Confirm New Password</label>
<input type="password" id="confirmPassword" bind:value={confirmPassword} required />
</div>
<button type="submit" disabled={isUpdating}>
{isUpdating ? 'Updating...' : 'Update Password'}
</button>
</form>
</section>
<section class="settings-section danger-section">
<h2>Account</h2>
<button onclick={handleLogout} class="btn btn-danger">
Logout
</button>
</section>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
header {
margin-bottom: 2rem;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
h1 {
margin: 0;
font-size: 1.75rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
h2 {
font-size: 1.1rem;
margin: 0 0 1rem;
}
.success {
background: rgba(34, 197, 94, 0.2);
border: 1px solid #22c55e;
color: #86efac;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.danger-section button {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
</style>

View File

@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

View File

@@ -0,0 +1,17 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
},
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

View File

@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

View File

@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});