first commit
Some checks failed
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

View File

@@ -0,0 +1 @@
{"specId": "fa730cf4-a14d-4f62-8993-fd7db6fe25cc", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,645 @@
# Design Document: Signal Dashboard
## Overview
Signal Dashboard is a React 18 + TypeScript SPA that consumes the existing Stock Data Backend REST API (`/api/v1/`). It provides authenticated users with views for watchlist monitoring, per-ticker analysis, trade setup scanning, composite-score rankings, and admin management.
The frontend lives in `frontend/` within the existing project root. Vite builds static assets to `frontend/dist/`, which Nginx serves on `signal.thiessen.io`. API requests to `/api/v1/` are proxied to the FastAPI backend — no CORS needed.
### Key Technical Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Build tool | Vite 5 | Fast HMR, native TS/React support, small output |
| Routing | React Router v6 | Standard, supports layout routes and guards |
| Server state | TanStack Query v5 | Caching, deduplication, background refetch |
| Client state | Zustand | Minimal auth store, no boilerplate |
| Styling | Tailwind CSS v3 | Utility-first, dark mode built-in, small bundle |
| Charts | Recharts | React-native charting, composable, lightweight |
| HTTP | Axios | Interceptors for auth/envelope unwrapping |
| Testing | Vitest + React Testing Library + fast-check | Vite-native test runner, property-based testing |
## Architecture
```mermaid
graph TD
subgraph Browser
Router[React Router]
Pages[Page Components]
Hooks[TanStack Query Hooks]
Store[Zustand Auth Store]
API[API Client - Axios]
end
Router --> Pages
Pages --> Hooks
Hooks --> API
API --> Store
Store --> API
subgraph Server
Nginx[Nginx - static files + proxy]
Backend[FastAPI Backend]
end
API -->|/api/v1/*| Nginx
Nginx -->|proxy_pass| Backend
Nginx -->|static| Browser
```
### Request Flow
1. Component mounts → calls a TanStack Query hook (e.g., `useWatchlist()`)
2. Hook calls an API client function (e.g., `api.watchlist.list()`)
3. Axios sends request with JWT Bearer header from Zustand store
4. Axios response interceptor unwraps `{ status, data, error }` envelope
5. On 401 → Zustand clears token, React Router redirects to `/login`
6. TanStack Query caches the result, component renders data
### Directory Structure
```
frontend/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.ts
├── postcss.config.js
├── src/
│ ├── main.tsx # App entry, providers
│ ├── App.tsx # Router + layout
│ ├── api/
│ │ ├── client.ts # Axios instance, interceptors
│ │ ├── auth.ts # login, register
│ │ ├── watchlist.ts # watchlist CRUD
│ │ ├── tickers.ts # ticker CRUD
│ │ ├── scores.ts # scores, rankings, weights
│ │ ├── trades.ts # trade setups
│ │ ├── ohlcv.ts # OHLCV data
│ │ ├── indicators.ts # technical indicators
│ │ ├── sr-levels.ts # support/resistance
│ │ ├── sentiment.ts # sentiment data
│ │ ├── fundamentals.ts # fundamental data
│ │ ├── ingestion.ts # manual data fetch
│ │ ├── admin.ts # admin endpoints
│ │ └── health.ts # health check
│ ├── hooks/
│ │ ├── useAuth.ts # login/register/logout mutations
│ │ ├── useWatchlist.ts # watchlist queries + mutations
│ │ ├── useTickers.ts # ticker queries + mutations
│ │ ├── useScores.ts # scores, rankings queries
│ │ ├── useTrades.ts # trade setup queries
│ │ ├── useTickerDetail.ts # parallel queries for detail view
│ │ └── useAdmin.ts # admin queries + mutations
│ ├── stores/
│ │ └── authStore.ts # Zustand: token, user, role
│ ├── pages/
│ │ ├── LoginPage.tsx
│ │ ├── RegisterPage.tsx
│ │ ├── WatchlistPage.tsx
│ │ ├── TickerDetailPage.tsx
│ │ ├── ScannerPage.tsx
│ │ ├── RankingsPage.tsx
│ │ └── AdminPage.tsx
│ ├── components/
│ │ ├── layout/
│ │ │ ├── AppShell.tsx # Sidebar + main content
│ │ │ ├── Sidebar.tsx
│ │ │ └── MobileNav.tsx
│ │ ├── auth/
│ │ │ └── ProtectedRoute.tsx
│ │ ├── charts/
│ │ │ └── CandlestickChart.tsx
│ │ ├── ui/
│ │ │ ├── ScoreCard.tsx
│ │ │ ├── Toast.tsx
│ │ │ ├── Skeleton.tsx
│ │ │ ├── Badge.tsx
│ │ │ └── ConfirmDialog.tsx
│ │ ├── watchlist/
│ │ │ ├── WatchlistTable.tsx
│ │ │ └── AddTickerForm.tsx
│ │ ├── scanner/
│ │ │ └── TradeTable.tsx
│ │ ├── rankings/
│ │ │ ├── RankingsTable.tsx
│ │ │ └── WeightsForm.tsx
│ │ ├── ticker/
│ │ │ ├── SentimentPanel.tsx
│ │ │ ├── FundamentalsPanel.tsx
│ │ │ ├── IndicatorSelector.tsx
│ │ │ └── SROverlay.tsx
│ │ └── admin/
│ │ ├── UserTable.tsx
│ │ ├── SettingsForm.tsx
│ │ ├── JobControls.tsx
│ │ └── DataCleanup.tsx
│ ├── lib/
│ │ ├── format.ts # Number/date formatting utilities
│ │ └── types.ts # Shared TypeScript interfaces
│ └── styles/
│ └── globals.css # Tailwind directives + custom vars
└── tests/
├── unit/
└── property/
```
## Components and Interfaces
### API Client (`src/api/client.ts`)
Central Axios instance with interceptors:
```typescript
// Axios instance configuration
const apiClient = axios.create({
baseURL: '/api/v1/',
timeout: 30_000,
headers: { 'Content-Type': 'application/json' },
});
// Request interceptor: attach JWT
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Response interceptor: unwrap envelope, handle 401
apiClient.interceptors.response.use(
(response) => {
const envelope = response.data as APIEnvelope;
if (envelope.status === 'error') throw new ApiError(envelope.error);
return envelope.data;
},
(error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
const msg = error.response?.data?.error ?? error.message ?? 'Network error';
throw new ApiError(msg);
}
);
```
### Auth Store (`src/stores/authStore.ts`)
```typescript
interface AuthState {
token: string | null;
username: string | null;
role: 'admin' | 'user' | null;
login: (token: string) => void;
logout: () => void;
}
```
- `login()` decodes the JWT payload to extract `sub` (username) and `role`, stores token in `localStorage`
- `logout()` clears token from state and `localStorage`, TanStack Query cache is cleared on logout
### Protected Route (`src/components/auth/ProtectedRoute.tsx`)
```typescript
// Wraps routes that require authentication
// Props: requireAdmin?: boolean
// If no token → redirect to /login
// If requireAdmin && role !== 'admin' → redirect to /watchlist
```
### Router Layout
```typescript
// Route structure
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route element={<ProtectedRoute />}>
<Route element={<AppShell />}>
<Route path="/" element={<Navigate to="/watchlist" />} />
<Route path="/watchlist" element={<WatchlistPage />} />
<Route path="/ticker/:symbol" element={<TickerDetailPage />} />
<Route path="/scanner" element={<ScannerPage />} />
<Route path="/rankings" element={<RankingsPage />} />
<Route element={<ProtectedRoute requireAdmin />}>
<Route path="/admin" element={<AdminPage />} />
</Route>
</Route>
</Route>
</Routes>
```
### TanStack Query Hooks Pattern
Each domain has a hook file that exports query/mutation hooks:
```typescript
// Example: useWatchlist.ts
export function useWatchlist() {
return useQuery({
queryKey: ['watchlist'],
queryFn: () => api.watchlist.list(),
});
}
export function useAddToWatchlist() {
const qc = useQueryClient();
return useMutation({
mutationFn: (symbol: string) => api.watchlist.add(symbol),
onSuccess: () => qc.invalidateQueries({ queryKey: ['watchlist'] }),
});
}
```
### Key UI Components
**ScoreCard**: Displays composite score with a colored ring/bar (green > 70, yellow 40-70, red < 40) and expandable dimension breakdown.
**CandlestickChart**: Recharts `ComposedChart` with custom `Bar` shapes for OHLCV candles. S/R levels rendered as `ReferenceLine` components with color coding (green = support, red = resistance).
**Toast System**: Lightweight toast using React context + portal. Auto-dismiss after 4 seconds. Error toasts in red, success in green.
**Skeleton**: Tailwind `animate-pulse` placeholder blocks matching the shape of cards/tables during loading states.
### Formatting Utilities (`src/lib/format.ts`)
```typescript
formatPrice(n: number): string // "1,234.56"
formatPercent(n: number): string // "12.34%"
formatLargeNumber(n: number): string // "1.23B", "456.7M", "12.3K"
formatDate(d: string): string // "Jan 15, 2025"
formatDateTime(d: string): string // "Jan 15, 2025 2:30 PM"
```
## Data Models
### TypeScript Interfaces (`src/lib/types.ts`)
```typescript
// API envelope (before unwrapping)
interface APIEnvelope<T = unknown> {
status: 'success' | 'error';
data: T | null;
error: string | null;
}
// Auth
interface TokenResponse {
access_token: string;
token_type: string;
}
// Watchlist
interface WatchlistEntry {
symbol: string;
entry_type: 'auto' | 'manual';
composite_score: number | null;
dimensions: DimensionScore[];
rr_ratio: number | null;
rr_direction: string | null;
sr_levels: SRLevelSummary[];
added_at: string;
}
interface DimensionScore {
dimension: string;
score: number;
}
interface SRLevelSummary {
price_level: number;
type: 'support' | 'resistance';
strength: number;
}
// OHLCV
interface OHLCVBar {
id: number;
ticker_id: number;
date: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
created_at: string;
}
// Scores
interface ScoreResponse {
symbol: string;
composite_score: number | null;
composite_stale: boolean;
weights: Record<string, number>;
dimensions: DimensionScoreDetail[];
missing_dimensions: string[];
computed_at: string | null;
}
interface DimensionScoreDetail {
dimension: string;
score: number;
is_stale: boolean;
computed_at: string | null;
}
interface RankingEntry {
symbol: string;
composite_score: number;
dimensions: DimensionScoreDetail[];
}
interface RankingsResponse {
rankings: RankingEntry[];
weights: Record<string, number>;
}
// Trade Setups
interface TradeSetup {
id: number;
symbol: string;
direction: string;
entry_price: number;
stop_loss: number;
target: number;
rr_ratio: number;
composite_score: number;
detected_at: string;
}
// S/R Levels
interface SRLevel {
id: number;
price_level: number;
type: 'support' | 'resistance';
strength: number;
detection_method: string;
created_at: string;
}
interface SRLevelResponse {
symbol: string;
levels: SRLevel[];
count: number;
}
// Sentiment
interface SentimentScore {
id: number;
classification: 'bullish' | 'bearish' | 'neutral';
confidence: number;
source: string;
timestamp: string;
}
interface SentimentResponse {
symbol: string;
scores: SentimentScore[];
count: number;
dimension_score: number | null;
lookback_hours: number;
}
// Fundamentals
interface FundamentalResponse {
symbol: string;
pe_ratio: number | null;
revenue_growth: number | null;
earnings_surprise: number | null;
market_cap: number | null;
fetched_at: string | null;
}
// Indicators
interface IndicatorResult {
indicator_type: string;
values: Record<string, unknown>;
score: number;
bars_used: number;
}
interface EMACrossResult {
short_ema: number;
long_ema: number;
short_period: number;
long_period: number;
signal: 'bullish' | 'bearish' | 'neutral';
}
// Tickers
interface Ticker {
id: number;
symbol: string;
created_at: string;
}
// Admin
interface AdminUser {
id: number;
username: string;
role: string;
has_access: boolean;
created_at: string | null;
updated_at: string | null;
}
interface SystemSetting {
key: string;
value: string;
updated_at: string | null;
}
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Token storage round-trip
*For any* valid JWT token string, storing it via `authStore.login(token)` and then reading `authStore.token` and `localStorage.getItem('token')` should both return the original token string.
**Validates: Requirements 1.1, 1.6**
### Property 2: Bearer token attachment
*For any* non-null token in the auth store, every request made through the API client should include an `Authorization` header with value `Bearer {token}`.
**Validates: Requirements 1.3, 12.3**
### Property 3: Registration form validation
*For any* username string shorter than 1 character or password string shorter than 6 characters, the registration form should reject submission. *For any* username of length >= 1 and password of length >= 6, the form should allow submission.
**Validates: Requirements 1.2**
### Property 4: Route protection based on auth state
*For any* protected route path, if no token exists in the auth store, navigation should redirect to `/login`. If a valid token exists, navigation should render the protected component.
**Validates: Requirements 2.1, 2.2**
### Property 5: API envelope unwrapping
*For any* API response with `status: "success"`, the API client should return the `data` field. *For any* API response with `status: "error"`, the API client should throw an error containing the `error` field message.
**Validates: Requirements 12.2**
### Property 6: Watchlist entry rendering completeness
*For any* watchlist entry, the rendered output should contain the symbol, entry type (with a visual badge distinguishing "auto" from "manual"), composite score, dimension scores, R:R ratio, R:R direction, and S/R levels.
**Validates: Requirements 3.2, 3.7**
### Property 7: Symbol click navigation
*For any* symbol displayed in the watchlist table, scanner table, or rankings table, clicking that symbol should trigger navigation to `/ticker/{symbol}`.
**Validates: Requirements 3.6, 5.6, 6.4**
### Property 8: Score card rendering
*For any* score response with a composite score and dimension scores, the ScoreCard component should render the composite score value and one entry per dimension with its name and score.
**Validates: Requirements 4.4**
### Property 9: Sentiment panel rendering
*For any* sentiment response, the rendered SentimentPanel should display the classification, confidence value, and dimension score.
**Validates: Requirements 4.5**
### Property 10: Fundamentals panel rendering
*For any* fundamentals response, the rendered FundamentalsPanel should display P/E ratio, revenue growth, earnings surprise, and market cap (or a placeholder for null values).
**Validates: Requirements 4.6**
### Property 11: Trade setup rendering
*For any* trade setup, the rendered table row should contain the symbol, direction, entry price, stop loss, target, R:R ratio, composite score, and detection timestamp.
**Validates: Requirements 5.2**
### Property 12: Scanner filtering
*For any* list of trade setups, minimum R:R filter value, and direction filter selection: all displayed setups should have `rr_ratio >= minRR` and (if direction is not "both") `direction === selectedDirection`.
**Validates: Requirements 5.3, 5.4**
### Property 13: Scanner sorting
*For any* list of trade setups and a selected sort column, the displayed rows should be ordered by that column's values (ascending or descending based on sort direction).
**Validates: Requirements 5.5**
### Property 14: Rankings display order
*For any* rankings response, the rendered list should display entries in descending order by composite score, with each entry showing rank position, symbol, composite score, and all dimension scores.
**Validates: Requirements 6.1, 6.2**
### Property 15: Admin user table rendering
*For any* admin user record, the rendered table row should contain the username, role, and access status.
**Validates: Requirements 7.2**
### Property 16: Number formatting
*For any* finite number, `formatPrice` should produce a string with exactly 2 decimal places. `formatPercent` should produce a string ending with `%`. `formatLargeNumber` should produce a string with an appropriate suffix (`K` for thousands, `M` for millions, `B` for billions) for values >= 1000, and no suffix for smaller values.
**Validates: Requirements 13.4**
### Property 17: Weights form rendering
*For any* weights map (dimension name → number), the WeightsForm should render one labeled numeric input per dimension key.
**Validates: Requirements 11.1**
## Error Handling
### API Client Error Strategy
All errors flow through the Axios response interceptor and are surfaced via the Toast system:
| Error Type | Detection | Behavior |
|---|---|---|
| 401 Unauthorized | `error.response.status === 401` | Clear auth store, redirect to `/login` |
| API error envelope | `envelope.status === 'error'` | Throw `ApiError` with `envelope.error` message |
| Network error | No `error.response` | Throw `ApiError` with "Network error — check your connection" |
| Timeout | Axios timeout (30s) | Throw `ApiError` with "Request timed out" |
| Unknown | Catch-all | Throw `ApiError` with `error.message` fallback |
### Component-Level Error Handling
- **TanStack Query `onError`**: Each mutation hook passes errors to the toast system
- **Query error states**: Components check `isError` and render inline error messages
- **Ticker Detail partial failure**: Each data section (scores, sentiment, fundamentals, S/R, OHLCV) is an independent query. If one fails, the others still render. Failed sections show an inline error with a retry button.
- **Form validation**: Client-side validation before API calls (username length, password length, numeric inputs). Invalid submissions are blocked with inline field errors.
### Toast System
```typescript
type ToastType = 'success' | 'error' | 'info';
interface Toast {
id: string;
type: ToastType;
message: string;
}
// Auto-dismiss after 4 seconds
// Max 3 toasts visible at once (oldest dismissed first)
// Error toasts: red accent, Success: green accent, Info: blue accent
```
## Testing Strategy
### Testing Stack
| Tool | Purpose |
|---|---|
| Vitest | Test runner (Vite-native, fast) |
| React Testing Library | Component rendering + DOM queries |
| fast-check | Property-based testing |
| MSW (Mock Service Worker) | API mocking for integration tests |
### Unit Tests
Unit tests cover specific examples, edge cases, and integration points:
- **Auth flow**: Login stores token, logout clears token, 401 triggers logout
- **API client**: Envelope unwrapping for success/error, timeout config, Bearer header
- **Routing**: Unauthenticated redirect, admin-only route guard, non-admin redirect
- **Component rendering**: Each page renders with mock data, loading skeletons appear, error states display
- **Form validation**: Empty username rejected, short password rejected, valid inputs accepted
- **Confirmation dialog**: Delete ticker shows confirm before API call
- **Partial failure**: Ticker detail renders available sections when one query fails
### Property-Based Tests
Each correctness property maps to a single `fast-check` property test with minimum 100 iterations. Tests are tagged with the property reference:
```typescript
// Feature: signal-dashboard, Property 16: Number formatting
test.prop([fc.float({ min: -1e15, max: 1e15, noNaN: true })], (n) => {
const result = formatPrice(n);
expect(result).toMatch(/\.\d{2}$/);
});
```
Property tests focus on:
- **Pure functions**: `format.ts` utilities (Property 16)
- **Store logic**: Auth store token round-trip (Property 1)
- **API client interceptors**: Envelope unwrapping (Property 5), Bearer attachment (Property 2)
- **Filtering/sorting logic**: Scanner filter functions (Properties 12, 13)
- **Component rendering**: Given generated data, components render required fields (Properties 6, 811, 14, 15, 17)
- **Routing guards**: Protected route behavior based on auth state (Property 4)
### Test Configuration
- Vitest config in `frontend/vitest.config.ts` with jsdom environment
- `fast-check` configured with `{ numRuns: 100 }` minimum per property
- MSW handlers for all API endpoints used in integration tests
- Each property test tagged: `Feature: signal-dashboard, Property {N}: {title}`

View File

@@ -0,0 +1,200 @@
# Requirements Document
## Introduction
Signal Dashboard is a single-page application (SPA) frontend for the Stock Data Backend API. The Dashboard provides authenticated users with a visual interface to monitor watchlists, analyze individual tickers across multiple dimensions (technical, S/R, sentiment, fundamentals, momentum), scan for asymmetric risk:reward trade setups, view composite-score rankings, and manage system administration. The Dashboard consumes the existing REST API at `/api/v1/` and is served as static files by Nginx on the same domain (`signal.thiessen.io`).
Technology choice: React 18 + TypeScript + Vite, with TanStack Query for data fetching, Zustand for auth state, Tailwind CSS for styling, Recharts for charting, and React Router for navigation. This stack prioritizes maintainability, small bundle size, and a modern developer experience without framework bloat.
## Glossary
- **Dashboard**: The Signal Dashboard SPA frontend application
- **API_Client**: The HTTP client module that communicates with the backend REST API
- **Auth_Module**: The authentication subsystem handling login, registration, token storage, and token refresh
- **Watchlist_View**: The main overview page displaying the user's watchlist entries with enriched score data
- **Ticker_Detail_View**: The per-ticker analysis page showing price chart, indicators, S/R levels, sentiment, and fundamentals
- **Scanner_View**: The trade setup scanner page displaying R:R filtered setups
- **Rankings_View**: The page displaying all tickers sorted by composite score
- **Admin_Panel**: The administration interface for user management, job control, system settings, and data cleanup
- **Router**: The client-side routing module controlling navigation and access guards
- **Token_Store**: The client-side storage mechanism for JWT access tokens
- **Chart_Component**: The interactive price chart component rendering OHLCV candlestick data with overlays
- **Score_Card**: A UI component displaying a composite score and its dimension breakdown
- **Toast_System**: The notification subsystem displaying transient success/error messages to the user
## Requirements
### Requirement 1: JWT Authentication Flow
**User Story:** As a user, I want to log in and register so that I can access the dashboard securely.
#### Acceptance Criteria
1. WHEN a user submits valid credentials on the login form, THE Auth_Module SHALL send a POST request to `/api/v1/auth/login` and store the returned JWT token in the Token_Store
2. WHEN a user submits a registration form with a username (minimum 1 character) and password (minimum 6 characters), THE Auth_Module SHALL send a POST request to `/api/v1/auth/register` and display a success message via the Toast_System
3. WHILE a valid JWT token exists in the Token_Store, THE API_Client SHALL include the token as a Bearer authorization header on all subsequent API requests
4. WHEN the API returns a 401 Unauthorized response, THE Auth_Module SHALL clear the Token_Store and redirect the user to the login page
5. IF the login or registration request fails, THEN THE Auth_Module SHALL display the error message from the API response via the Toast_System
6. THE Token_Store SHALL persist the JWT token in browser localStorage so that sessions survive page reloads
### Requirement 2: Protected Routing and Role-Based Access
**User Story:** As a user, I want the app to enforce access control so that unauthenticated users cannot access protected pages and only admins can access admin features.
#### Acceptance Criteria
1. WHILE no valid JWT token exists in the Token_Store, THE Router SHALL redirect navigation to any protected route to the login page
2. WHILE a valid JWT token exists in the Token_Store, THE Router SHALL allow navigation to protected routes (Watchlist_View, Ticker_Detail_View, Scanner_View, Rankings_View)
3. WHILE the authenticated user has an admin role, THE Router SHALL allow navigation to the Admin_Panel
4. WHILE the authenticated user has a non-admin role, THE Router SHALL redirect navigation to the Admin_Panel to the Watchlist_View
5. THE Router SHALL provide a navigation sidebar or top bar with links to all accessible views for the authenticated user
### Requirement 3: Watchlist Overview
**User Story:** As a user, I want to see my watchlist with composite scores, dimension breakdowns, and R:R ratios so that I can quickly assess my tracked tickers.
#### Acceptance Criteria
1. WHEN the Watchlist_View loads, THE Dashboard SHALL fetch data from `GET /api/v1/watchlist` and display each entry as a card or row
2. THE Watchlist_View SHALL display for each entry: symbol, entry type (auto/manual), composite score, dimension scores, R:R ratio, R:R direction, and nearest S/R levels
3. WHEN a user clicks the add-to-watchlist control and enters a valid ticker symbol, THE Dashboard SHALL send a POST request to `/api/v1/watchlist/{symbol}` and refresh the watchlist
4. WHEN a user clicks the remove button on a watchlist entry, THE Dashboard SHALL send a DELETE request to `/api/v1/watchlist/{symbol}` and remove the entry from the display
5. IF the watchlist API request fails, THEN THE Dashboard SHALL display the error message via the Toast_System
6. WHEN a user clicks on a watchlist entry symbol, THE Router SHALL navigate to the Ticker_Detail_View for that symbol
7. THE Watchlist_View SHALL visually distinguish auto-populated entries from manual entries using a badge or label
### Requirement 4: Ticker Detail View
**User Story:** As a user, I want to see a comprehensive analysis of a single ticker including price chart, indicators, S/R levels, sentiment, and fundamentals so that I can make informed decisions.
#### Acceptance Criteria
1. WHEN the Ticker_Detail_View loads for a given symbol, THE Dashboard SHALL fetch data in parallel from: `GET /api/v1/ohlcv/{symbol}`, `GET /api/v1/scores/{symbol}`, `GET /api/v1/sr-levels/{symbol}`, `GET /api/v1/sentiment/{symbol}`, and `GET /api/v1/fundamentals/{symbol}`
2. THE Chart_Component SHALL render OHLCV data as a candlestick chart with date on the x-axis and price on the y-axis
3. THE Chart_Component SHALL overlay S/R levels as horizontal lines on the price chart, color-coded by type (support in green, resistance in red)
4. THE Ticker_Detail_View SHALL display the composite score and all dimension scores using Score_Card components
5. THE Ticker_Detail_View SHALL display sentiment data including classification (bullish/bearish/neutral), confidence, and the time-decay weighted dimension score
6. THE Ticker_Detail_View SHALL display fundamental data including P/E ratio, revenue growth, earnings surprise, and market cap
7. WHEN a user selects an indicator type (ADX, EMA, RSI, ATR, volume_profile, pivot_points), THE Dashboard SHALL fetch data from `GET /api/v1/indicators/{symbol}/{indicator_type}` and display the result with its normalized score
8. WHEN a user requests the EMA cross signal, THE Dashboard SHALL fetch data from `GET /api/v1/indicators/{symbol}/ema-cross` and display the signal (bullish/bearish/neutral) with short and long EMA values
9. IF any data fetch fails for the Ticker_Detail_View, THEN THE Dashboard SHALL display an inline error message for the failed section while rendering the remaining sections normally
### Requirement 5: Trade Setup Scanner
**User Story:** As a user, I want to scan for trade setups with favorable risk:reward ratios so that I can find asymmetric opportunities.
#### Acceptance Criteria
1. WHEN the Scanner_View loads, THE Dashboard SHALL fetch data from `GET /api/v1/trades` and display all trade setups in a sortable table
2. THE Scanner_View SHALL display for each trade setup: symbol, direction (long/short), entry price, stop loss, target, R:R ratio, composite score, and detection timestamp
3. THE Scanner_View SHALL allow the user to filter trade setups by minimum R:R ratio using a numeric input
4. THE Scanner_View SHALL allow the user to filter trade setups by direction (long, short, or both)
5. THE Scanner_View SHALL allow the user to sort the table by any column (R:R ratio, composite score, symbol, detection time)
6. WHEN a user clicks on a trade setup symbol, THE Router SHALL navigate to the Ticker_Detail_View for that symbol
### Requirement 6: Rankings View
**User Story:** As a user, I want to see all tickers ranked by composite score so that I can identify the strongest opportunities.
#### Acceptance Criteria
1. WHEN the Rankings_View loads, THE Dashboard SHALL fetch data from `GET /api/v1/rankings` and display tickers sorted by composite score descending
2. THE Rankings_View SHALL display for each ticker: rank position, symbol, composite score, and all dimension scores
3. THE Rankings_View SHALL display the current scoring weights used for composite calculation
4. WHEN a user clicks on a ranked ticker symbol, THE Router SHALL navigate to the Ticker_Detail_View for that symbol
### Requirement 7: Admin Panel — User Management
**User Story:** As an admin, I want to manage user accounts so that I can control access to the platform.
#### Acceptance Criteria
1. WHEN the Admin_Panel user management section loads, THE Dashboard SHALL fetch data from `GET /api/v1/admin/users` and display all users in a table
2. THE Admin_Panel SHALL display for each user: username, role, and access status
3. WHEN an admin clicks the create-user control and submits a username, password, role, and access flag, THE Dashboard SHALL send a POST request to `/api/v1/admin/users` and refresh the user list
4. WHEN an admin toggles a user's access status, THE Dashboard SHALL send a PUT request to `/api/v1/admin/users/{user_id}/access` with the new access flag
5. WHEN an admin resets a user's password, THE Dashboard SHALL send a PUT request to `/api/v1/admin/users/{user_id}/password` with the new password
6. IF any admin user management request fails, THEN THE Dashboard SHALL display the error message via the Toast_System
### Requirement 8: Admin Panel — System Settings and Jobs
**User Story:** As an admin, I want to manage system settings, scheduled jobs, and data cleanup so that I can maintain the platform.
#### Acceptance Criteria
1. WHEN the Admin_Panel settings section loads, THE Dashboard SHALL fetch data from `GET /api/v1/admin/settings` and display all settings as editable fields
2. WHEN an admin updates a system setting value, THE Dashboard SHALL send a PUT request to `/api/v1/admin/settings/{key}` with the new value
3. WHEN an admin toggles the registration setting, THE Dashboard SHALL send a PUT request to `/api/v1/admin/settings/registration` with the enabled flag
4. WHEN an admin toggles a scheduled job on or off, THE Dashboard SHALL send a PUT request to `/api/v1/admin/jobs/{job_name}/toggle` with the enabled flag
5. WHEN an admin triggers a scheduled job manually, THE Dashboard SHALL send a POST request to `/api/v1/admin/jobs/{job_name}/trigger` and display a confirmation via the Toast_System
6. WHEN an admin submits a data cleanup request with an older-than-days value, THE Dashboard SHALL send a POST request to `/api/v1/admin/data/cleanup` with the specified value and display the result via the Toast_System
### Requirement 9: Ticker Management
**User Story:** As a user, I want to add and remove tickers from the system so that I can track the stocks I care about.
#### Acceptance Criteria
1. WHEN a user submits a new ticker symbol via the add-ticker form, THE Dashboard SHALL send a POST request to `/api/v1/tickers` with the symbol and refresh the ticker list
2. WHEN a user views the ticker list, THE Dashboard SHALL fetch data from `GET /api/v1/tickers` and display all registered tickers
3. WHEN a user clicks the delete button on a ticker, THE Dashboard SHALL display a confirmation dialog before sending a DELETE request to `/api/v1/tickers/{symbol}`
4. IF a ticker deletion or creation request fails, THEN THE Dashboard SHALL display the error message via the Toast_System
5. WHEN a ticker is successfully deleted, THE Dashboard SHALL remove the ticker from the displayed list without requiring a full page reload
### Requirement 10: Data Ingestion Trigger
**User Story:** As a user, I want to manually trigger data ingestion for a specific ticker so that I can get fresh data on demand.
#### Acceptance Criteria
1. WHEN a user clicks the fetch-data button on the Ticker_Detail_View, THE Dashboard SHALL send a POST request to `/api/v1/ingestion/fetch/{symbol}`
2. WHILE the ingestion request is in progress, THE Dashboard SHALL display a loading indicator on the fetch-data button
3. WHEN the ingestion request completes successfully, THE Dashboard SHALL display a success message via the Toast_System and refresh the OHLCV data on the Ticker_Detail_View
4. IF the ingestion request fails, THEN THE Dashboard SHALL display the error message via the Toast_System
### Requirement 11: Score Weight Configuration
**User Story:** As a user, I want to adjust the scoring dimension weights so that I can customize the composite score calculation to my strategy.
#### Acceptance Criteria
1. THE Dashboard SHALL display the current scoring weights on the Rankings_View with editable numeric inputs for each dimension
2. WHEN a user modifies one or more weight values and submits the form, THE Dashboard SHALL send a PUT request to `/api/v1/scores/weights` with the updated weights map
3. WHEN the weight update succeeds, THE Dashboard SHALL refresh the rankings data to reflect the new composite scores
4. IF the weight update request fails, THEN THE Dashboard SHALL display the error message via the Toast_System
### Requirement 12: API Client and Error Handling
**User Story:** As a developer, I want a centralized API client with consistent error handling so that all API interactions follow the same patterns.
#### Acceptance Criteria
1. THE API_Client SHALL send all requests to the base URL `/api/v1/` using the JSON content type
2. THE API_Client SHALL unwrap the API envelope (`{ status, data, error }`) and return the `data` field on success or throw an error with the `error` field on failure
3. THE API_Client SHALL attach the JWT Bearer token from the Token_Store to every authenticated request
4. WHEN the API_Client receives a network error or timeout, THE API_Client SHALL throw a descriptive error that the calling component can display via the Toast_System
5. THE API_Client SHALL set a request timeout of 30 seconds for all API calls
### Requirement 13: Responsive Layout and Visual Design
**User Story:** As a user, I want the dashboard to have a clean, modern interface that works on desktop and tablet screens so that I can use it comfortably.
#### Acceptance Criteria
1. THE Dashboard SHALL use a sidebar navigation layout on screens wider than 1024 pixels and a collapsible hamburger menu on narrower screens
2. THE Dashboard SHALL use a dark color scheme with accent colors for positive (green) and negative (red) values consistent with financial data conventions
3. THE Dashboard SHALL apply smooth transitions (duration 150ms to 300ms) for page navigation, modal openings, and interactive element state changes
4. THE Dashboard SHALL display numeric financial values with appropriate formatting: prices to 2 decimal places, percentages with a percent sign, large numbers with abbreviations (K, M, B)
5. THE Dashboard SHALL display loading skeleton placeholders while data is being fetched from the API
### Requirement 14: Static SPA Build and Deployment
**User Story:** As a developer, I want the frontend to build as static files that Nginx can serve alongside the backend API so that deployment is simple.
#### Acceptance Criteria
1. THE Dashboard SHALL produce a static build output (HTML, CSS, JS) in a `dist/` directory via a single build command
2. THE Dashboard SHALL use hash-based filenames for JS and CSS assets to enable long-term browser caching
3. THE Dashboard SHALL support client-side routing with a fallback to `index.html` for all non-API routes (Nginx `try_files` configuration)
4. THE Dashboard SHALL proxy API requests to `/api/v1/` on the same origin, requiring no CORS configuration in production
5. WHEN the `GET /api/v1/health` endpoint returns a success response, THE Dashboard SHALL consider the backend available

View File

@@ -0,0 +1,238 @@
# Implementation Plan: Signal Dashboard
## Overview
Incremental build of the Signal Dashboard SPA in `frontend/`. Each phase wires up end-to-end before moving on, so there's always a runnable app. Backend API is already live — we consume it as-is.
## Tasks
- [x] 1. Scaffold Vite + React + TypeScript project
- [x] 1.1 Initialize `frontend/` with Vite React-TS template, install dependencies (react, react-dom, react-router-dom, @tanstack/react-query, zustand, axios, recharts, tailwindcss, postcss, autoprefixer)
- Create `package.json`, `tsconfig.json`, `vite.config.ts`, `tailwind.config.ts`, `postcss.config.js`
- Configure Vite proxy for `/api/v1/` to backend during dev
- Configure Tailwind with dark mode class strategy
- Create `src/main.tsx`, `src/App.tsx`, `src/styles/globals.css` with Tailwind directives
- _Requirements: 14.1, 14.4, 13.2_
- [x] 1.2 Create shared TypeScript interfaces and formatting utilities
- Create `src/lib/types.ts` with all data model interfaces (APIEnvelope, TokenResponse, WatchlistEntry, OHLCVBar, ScoreResponse, TradeSetup, SRLevel, SentimentResponse, FundamentalResponse, IndicatorResult, EMACrossResult, Ticker, AdminUser, SystemSetting, etc.)
- Create `src/lib/format.ts` with `formatPrice`, `formatPercent`, `formatLargeNumber`, `formatDate`, `formatDateTime`
- _Requirements: 13.4_
- [ ]* 1.3 Write property tests for formatting utilities
- **Property 16: Number formatting**
- **Validates: Requirements 13.4**
- Install vitest, @testing-library/react, fast-check as dev dependencies
- Create `frontend/vitest.config.ts` with jsdom environment
- Create `frontend/tests/property/format.test.ts`
- Test `formatPrice` always produces 2 decimal places, `formatPercent` ends with `%`, `formatLargeNumber` uses correct suffix
- [x] 2. API client and auth store
- [x] 2.1 Create Axios API client with interceptors
- Create `src/api/client.ts` with base URL `/api/v1/`, 30s timeout, JSON content type
- Add request interceptor to attach Bearer token from auth store
- Add response interceptor to unwrap `{ status, data, error }` envelope
- Add 401 handler that clears auth store and redirects to login
- Create `ApiError` class for typed error handling
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5_
- [x] 2.2 Create Zustand auth store
- Create `src/stores/authStore.ts` with token, username, role state
- `login(token)` decodes JWT payload, extracts `sub` and `role`, persists to localStorage
- `logout()` clears state and localStorage
- Initialize from localStorage on store creation for session persistence
- _Requirements: 1.1, 1.6_
- [x] 2.3 Create API module files for each domain
- Create `src/api/auth.ts` (login, register)
- Create `src/api/watchlist.ts` (list, add, remove)
- Create `src/api/tickers.ts` (list, create, delete)
- Create `src/api/scores.ts` (getScores, getRankings, updateWeights)
- Create `src/api/trades.ts` (list)
- Create `src/api/ohlcv.ts` (getOHLCV)
- Create `src/api/indicators.ts` (getIndicator, getEMACross)
- Create `src/api/sr-levels.ts` (getLevels)
- Create `src/api/sentiment.ts` (getSentiment)
- Create `src/api/fundamentals.ts` (getFundamentals)
- Create `src/api/ingestion.ts` (fetchData)
- Create `src/api/admin.ts` (users CRUD, settings, jobs, cleanup)
- Create `src/api/health.ts` (check)
- _Requirements: 12.1, 12.2_
- [ ]* 2.4 Write property tests for API client and auth store
- **Property 1: Token storage round-trip**
- **Property 2: Bearer token attachment**
- **Property 5: API envelope unwrapping**
- **Validates: Requirements 1.1, 1.3, 1.6, 12.2, 12.3**
- [x] 3. Checkpoint — Verify foundation
- Ensure all tests pass, ask the user if questions arise.
- [x] 4. Routing, layout, and auth pages
- [x] 4.1 Create ProtectedRoute component and router setup
- Create `src/components/auth/ProtectedRoute.tsx` — redirects to `/login` if no token, redirects non-admin away from admin routes
- Set up React Router in `src/App.tsx` with route structure from design (login, register, protected shell with watchlist, ticker detail, scanner, rankings, admin)
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 4.2 Create AppShell layout with sidebar navigation
- Create `src/components/layout/AppShell.tsx` — sidebar + main content area with `<Outlet />`
- Create `src/components/layout/Sidebar.tsx` — nav links to watchlist, scanner, rankings, admin (admin link only if role is admin)
- Create `src/components/layout/MobileNav.tsx` — hamburger menu for screens < 1024px
- Apply dark color scheme with Tailwind dark mode classes
- Add smooth transitions (150-300ms) for navigation and interactive elements
- _Requirements: 2.5, 13.1, 13.2, 13.3_
- [x] 4.3 Create Login and Register pages
- Create `src/pages/LoginPage.tsx` with username/password form, calls `useAuth().login` mutation
- Create `src/pages/RegisterPage.tsx` with username (min 1 char) / password (min 6 chars) validation, calls `useAuth().register` mutation
- Create `src/hooks/useAuth.ts` with login/register/logout mutations using TanStack Query
- Display API errors via toast on failure, redirect to watchlist on login success
- _Requirements: 1.1, 1.2, 1.4, 1.5_
- [x] 4.4 Create shared UI components
- Create `src/components/ui/Toast.tsx` — toast context + portal, auto-dismiss 4s, max 3 visible, color-coded (red/green/blue)
- Create `src/components/ui/Skeleton.tsx` — Tailwind `animate-pulse` placeholder blocks
- Create `src/components/ui/Badge.tsx` — small label component for entry types
- Create `src/components/ui/ConfirmDialog.tsx` — modal confirmation dialog
- Create `src/components/ui/ScoreCard.tsx` — composite score display with colored ring (green > 70, yellow 40-70, red < 40) and dimension breakdown
- _Requirements: 13.3, 13.5, 1.5_
- [ ]* 4.5 Write property tests for routing and registration validation
- **Property 3: Registration form validation**
- **Property 4: Route protection based on auth state**
- **Validates: Requirements 1.2, 2.1, 2.2**
- [x] 5. Checkpoint — Verify auth flow and navigation
- Ensure all tests pass, ask the user if questions arise.
- [x] 6. Watchlist view
- [x] 6.1 Create TanStack Query hooks for watchlist
- Create `src/hooks/useWatchlist.ts` with `useWatchlist()` query, `useAddToWatchlist()` mutation, `useRemoveFromWatchlist()` mutation
- Invalidate watchlist query on add/remove success
- Surface errors to toast system
- _Requirements: 3.1, 3.3, 3.4, 3.5_
- [x] 6.2 Create WatchlistPage and sub-components
- Create `src/pages/WatchlistPage.tsx` — fetches watchlist, renders table/cards, loading skeletons, error state
- Create `src/components/watchlist/WatchlistTable.tsx` — displays symbol (clickable → `/ticker/{symbol}`), entry type badge (auto/manual), composite score, dimension scores, R:R ratio, R:R direction, nearest S/R levels, remove button
- Create `src/components/watchlist/AddTickerForm.tsx` — input + submit to add symbol to watchlist
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.6, 3.7_
- [ ]* 6.3 Write property tests for watchlist rendering
- **Property 6: Watchlist entry rendering completeness**
- **Property 7: Symbol click navigation** (watchlist portion)
- **Validates: Requirements 3.2, 3.6, 3.7**
- [x] 7. Ticker detail view
- [x] 7.1 Create TanStack Query hooks for ticker detail
- Create `src/hooks/useTickerDetail.ts` with parallel queries for OHLCV, scores, S/R levels, sentiment, fundamentals
- Each query is independent — partial failure renders available sections
- _Requirements: 4.1, 4.9_
- [x] 7.2 Create TickerDetailPage with chart and data panels
- Create `src/pages/TickerDetailPage.tsx` — orchestrates parallel data fetching, renders sections with independent loading/error states
- Create `src/components/charts/CandlestickChart.tsx` — Recharts ComposedChart with custom Bar shapes for OHLCV candles, date x-axis, price y-axis
- Create `src/components/ticker/SROverlay.tsx` — renders S/R levels as ReferenceLine components on chart (green = support, red = resistance)
- Render ScoreCard for composite + dimension scores
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.9_
- [x] 7.3 Create sentiment, fundamentals, and indicator panels
- Create `src/components/ticker/SentimentPanel.tsx` — displays classification, confidence, dimension score
- Create `src/components/ticker/FundamentalsPanel.tsx` — displays P/E, revenue growth, earnings surprise, market cap (placeholder for nulls)
- Create `src/components/ticker/IndicatorSelector.tsx` — dropdown to select indicator type (ADX, EMA, RSI, ATR, volume_profile, pivot_points), fetches from `/api/v1/indicators/{symbol}/{type}`, displays result with normalized score. Includes EMA cross signal display.
- _Requirements: 4.5, 4.6, 4.7, 4.8_
- [x] 7.4 Add data ingestion trigger to ticker detail
- Add fetch-data button to TickerDetailPage
- POST to `/api/v1/ingestion/fetch/{symbol}`, show loading indicator on button, toast on success/failure, refresh OHLCV data on success
- _Requirements: 10.1, 10.2, 10.3, 10.4_
- [ ]* 7.5 Write property tests for ticker detail components
- **Property 8: Score card rendering**
- **Property 9: Sentiment panel rendering**
- **Property 10: Fundamentals panel rendering**
- **Validates: Requirements 4.4, 4.5, 4.6**
- [x] 8. Checkpoint — Verify watchlist and ticker detail
- Ensure all tests pass, ask the user if questions arise.
- [x] 9. Scanner view
- [x] 9.1 Create TanStack Query hooks and scanner page
- Create `src/hooks/useTrades.ts` with `useTrades()` query
- Create `src/pages/ScannerPage.tsx` — fetches trade setups, renders filter controls and table, loading skeletons
- Create `src/components/scanner/TradeTable.tsx` — sortable table displaying symbol (clickable → `/ticker/{symbol}`), direction, entry price, stop loss, target, R:R ratio, composite score, detection timestamp
- Add filter controls: minimum R:R numeric input, direction dropdown (long/short/both)
- Add column sorting (R:R ratio, composite score, symbol, detection time) with ascending/descending toggle
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6_
- [ ]* 9.2 Write property tests for scanner filtering and sorting
- **Property 11: Trade setup rendering**
- **Property 12: Scanner filtering**
- **Property 13: Scanner sorting**
- **Validates: Requirements 5.2, 5.3, 5.4, 5.5**
- [x] 10. Rankings view
- [x] 10.1 Create TanStack Query hooks and rankings page
- Create `src/hooks/useScores.ts` with `useRankings()` query, `useUpdateWeights()` mutation
- Create `src/pages/RankingsPage.tsx` — fetches rankings, renders table sorted by composite score descending, displays current weights
- Create `src/components/rankings/RankingsTable.tsx` — displays rank position, symbol (clickable → `/ticker/{symbol}`), composite score, all dimension scores
- Create `src/components/rankings/WeightsForm.tsx` — editable numeric inputs per dimension, submit updates weights via PUT, refreshes rankings on success
- _Requirements: 6.1, 6.2, 6.3, 6.4, 11.1, 11.2, 11.3, 11.4_
- [ ]* 10.2 Write property tests for rankings and weights
- **Property 14: Rankings display order**
- **Property 17: Weights form rendering**
- **Validates: Requirements 6.1, 6.2, 11.1**
- [x] 11. Checkpoint — Verify scanner and rankings
- Ensure all tests pass, ask the user if questions arise.
- [x] 12. Ticker management
- [x] 12.1 Create TanStack Query hooks and ticker management UI
- Create `src/hooks/useTickers.ts` with `useTickers()` query, `useAddTicker()` mutation, `useDeleteTicker()` mutation
- Add ticker list display to an appropriate location (e.g., admin page or dedicated section)
- Add ticker form for adding new symbols
- Delete button triggers ConfirmDialog before sending DELETE request
- Remove ticker from display on successful delete without full page reload
- _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5_
- [x] 13. Admin panel
- [x] 13.1 Create admin hooks and user management section
- Create `src/hooks/useAdmin.ts` with queries and mutations for users, settings, jobs, cleanup
- Create `src/pages/AdminPage.tsx` — tabbed layout with user management, settings, jobs, data cleanup sections
- Create `src/components/admin/UserTable.tsx` — displays username, role, access status; toggle access, reset password controls
- Add create-user form (username, password, role, access flag)
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_
- [x] 13.2 Create settings, jobs, and data cleanup sections
- Create `src/components/admin/SettingsForm.tsx` — editable fields for each setting, registration toggle
- Create `src/components/admin/JobControls.tsx` — toggle on/off per job, manual trigger button, toast confirmation
- Create `src/components/admin/DataCleanup.tsx` — older-than-days input, submit cleanup, display result via toast
- _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6_
- [ ]* 13.3 Write property test for admin user table rendering
- **Property 15: Admin user table rendering**
- **Validates: Requirements 7.2**
- [x] 14. Final wiring and polish
- [x] 14.1 Add health check and loading states
- Create health check query using `GET /api/v1/health` — display backend status indicator in sidebar
- Ensure all pages show Skeleton placeholders during loading
- Ensure all mutation errors surface through Toast system consistently
- _Requirements: 14.5, 13.5, 12.4_
- [x] 14.2 Configure production build
- Verify `vite build` outputs to `frontend/dist/` with hashed filenames
- Add Nginx config snippet in comments or README for `try_files $uri $uri/ /index.html` and `/api/v1/` proxy
- _Requirements: 14.1, 14.2, 14.3, 14.4_
- [x] 15. Final checkpoint — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional property test tasks and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Backend API is already running — no backend changes needed
- All 17 correctness properties are covered across optional test tasks
- Checkpoints are placed after each major phase for incremental validation