Skip to content

Commit 08ad4d3

Browse files
committed
add login page
Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent 5a087c0 commit 08ad4d3

21 files changed

Lines changed: 1659 additions & 1419 deletions

dashboard/Caddyfile

Lines changed: 0 additions & 17 deletions
This file was deleted.

dashboard/DEPLOYMENT.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Dashboard Deployment Guide
2+
3+
The AlphaTrion dashboard supports multiple deployment scenarios:
4+
5+
## Local Development
6+
7+
For local development with the proxy:
8+
9+
```bash
10+
npm run dev
11+
```
12+
13+
The dashboard will use the Vite proxy to forward API requests to `http://localhost:8000`.
14+
15+
## Docker Deployment
16+
17+
### Build the Docker image:
18+
19+
```bash
20+
docker build -t alphatrion-dashboard:latest .
21+
```
22+
23+
### Run with environment variable:
24+
25+
```bash
26+
docker run -p 8080:8080 \
27+
-e VITE_API_URL=http://localhost:8000 \
28+
alphatrion-dashboard:latest
29+
```
30+
31+
## Kubernetes Deployment
32+
33+
The dashboard can be deployed separately from the backend in Kubernetes.
34+
35+
### Configure the backend API URL in `values.yaml`:
36+
37+
```yaml
38+
dashboard:
39+
env:
40+
# For internal cluster communication
41+
apiUrl: "http://alphatrion-server:8000"
42+
43+
# For external access through ingress
44+
# apiUrl: "https://api.example.com"
45+
```
46+
47+
### Deploy with Helm:
48+
49+
```bash
50+
helm install alphatrion ./charts/alphatrion \
51+
--set dashboard.env.apiUrl=http://alphatrion-server:8000
52+
```
53+
54+
## How It Works
55+
56+
The dashboard supports three layers of configuration (in order of precedence):
57+
58+
1. **Runtime config** (Kubernetes): `window.ENV.VITE_API_URL` - injected by `entrypoint.sh` at container startup
59+
2. **Build-time env** (Docker): `import.meta.env.VITE_API_URL` - set during `npm run build`
60+
3. **Relative URL** (Local dev): Empty string - uses Vite proxy
61+
62+
### Runtime Configuration
63+
64+
In production (Docker/Kubernetes), the `entrypoint.sh` script generates a `/config.js` file with:
65+
66+
```javascript
67+
window.ENV = {
68+
VITE_API_URL: "http://alphatrion-server:8000"
69+
};
70+
```
71+
72+
This is loaded before the main application and provides runtime configuration without rebuilding the image.
73+
74+
## Environment Variables
75+
76+
- `VITE_API_URL`: Backend API base URL (e.g., `http://alphatrion-server:8000` or `https://api.example.com`)
77+
- Leave empty for local development with proxy
78+
- Set in Docker run command or Kubernetes deployment for production
79+
80+
## Architecture
81+
82+
```
83+
┌─────────────────────┐
84+
│ User Browser │
85+
└──────────┬──────────┘
86+
87+
│ HTTPS
88+
89+
┌──────────▼──────────┐
90+
│ Ingress/LB │
91+
│ (optional) │
92+
└──────────┬──────────┘
93+
94+
┌─────┴─────┐
95+
│ │
96+
┌────▼────┐ ┌───▼─────┐
97+
│Dashboard│ │ Backend │
98+
│ (nginx) │ │ (API) │
99+
│ :8080 │ │ :8000 │
100+
└─────────┘ └─────────┘
101+
```
102+
103+
The dashboard is a static single-page application served by nginx. It communicates with the backend API using the configured `VITE_API_URL`.
104+
105+
## Why nginx?
106+
107+
The dashboard needs a web server to:
108+
1. **Serve static files over HTTP** - HTML, JavaScript, CSS, images
109+
2. **Handle SPA routing** - Return `index.html` for all routes (e.g., `/experiments/123`) so React Router can handle client-side routing
110+
111+
nginx is the industry-standard choice for serving static content in Kubernetes environments.

dashboard/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
<body>
1212
<div id="root"></div>
13+
<!-- Load runtime configuration (generated by entrypoint.sh in production) -->
14+
<script src="/config.js"></script>
1315
<!-- Vite entry points to src/main.tsx -->
1416
<script type="module" src="/src/main.tsx"></script>
1517
</body>

dashboard/public/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Runtime configuration
2+
// This file is generated by entrypoint.sh in production
3+
// For local development, it's empty (uses import.meta.env instead)
4+
window.ENV = window.ENV || {};

dashboard/src/App.tsx

Lines changed: 88 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { useEffect, useState } from 'react';
2-
import { Route, Routes } from 'react-router-dom';
2+
import { Route, Routes, useNavigate } from 'react-router-dom';
33
import { useQueryClient } from '@tanstack/react-query';
4-
import { getUserId } from './lib/config';
5-
import { graphqlQuery, queries } from './lib/graphql-client';
64
import { User, UserProvider } from './context/user-context';
75
import { useTeamContext } from './context/team-context';
86
import { Layout } from './components/layout/layout';
@@ -17,22 +15,80 @@ import { AgentDetailPage } from './pages/agents/[id]';
1715
import { SessionDetailPage } from './pages/sessions/[id]';
1816
import { DatasetsPage } from './pages/datasets';
1917
import { ArtifactsPage } from './pages/artifacts';
18+
import { LoginPage } from './pages/login';
2019
import type { Team } from './types';
2120

21+
// Helper to decode JWT
22+
function decodeJWT(token: string): any {
23+
try {
24+
const base64Url = token.split('.')[1];
25+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
26+
const jsonPayload = decodeURIComponent(
27+
atob(base64)
28+
.split('')
29+
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
30+
.join('')
31+
);
32+
return JSON.parse(jsonPayload);
33+
} catch {
34+
return null;
35+
}
36+
}
37+
2238
function App() {
2339
const [currentUser, setCurrentUser] = useState<User | null>(null);
2440
const [loading, setLoading] = useState(true);
2541
const [error, setError] = useState<Error | null>(null);
2642
const { selectedTeamId, setSelectedTeamId } = useTeamContext();
2743
const queryClient = useQueryClient();
44+
const navigate = useNavigate();
2845

2946
useEffect(() => {
3047
async function initialize() {
3148
try {
32-
// Step 1: Get userId and orgId from config
33-
const config = await fetch('/api/config').then(res => res.json());
34-
const userId = config.userId;
35-
const orgId = config.orgId;
49+
// Check for JWT token
50+
const token = localStorage.getItem('alphatrion_token');
51+
const storedUser = localStorage.getItem('alphatrion_user');
52+
53+
console.log('Initializing app...', {
54+
hasToken: !!token,
55+
hasUser: !!storedUser,
56+
currentPath: window.location.pathname
57+
});
58+
59+
if (!token || !storedUser) {
60+
// No token, redirect to login (only if not already there)
61+
console.log('No token or user, redirecting to login');
62+
if (window.location.pathname !== '/login') {
63+
navigate('/login');
64+
}
65+
setLoading(false);
66+
return;
67+
}
68+
69+
// Decode JWT to check expiration
70+
const payload = decodeJWT(token);
71+
const isExpired = !payload || payload.exp * 1000 < Date.now();
72+
console.log('Token validation:', { isExpired, exp: payload?.exp });
73+
74+
if (isExpired) {
75+
// Token expired, clear and redirect to login
76+
console.log('Token expired, clearing and redirecting');
77+
localStorage.removeItem('alphatrion_token');
78+
localStorage.removeItem('alphatrion_user');
79+
if (window.location.pathname !== '/login') {
80+
navigate('/login');
81+
}
82+
setLoading(false);
83+
return;
84+
}
85+
86+
console.log('User authenticated, loading dashboard');
87+
88+
// Parse stored user info (already has teams from login response)
89+
const user = JSON.parse(storedUser);
90+
const userId = user.id;
91+
const orgId = payload.org_id;
3692

3793
// Check if user ID has changed from previous session
3894
const previousUserId = localStorage.getItem('alphatrion_user_id');
@@ -41,54 +97,32 @@ function App() {
4197
console.log('User ID changed, clearing cache');
4298
queryClient.clear();
4399
}
100+
44101
// Store current user ID and org ID for GraphQL headers
45102
localStorage.setItem('alphatrion_user_id', userId);
46-
if (orgId) {
47-
localStorage.setItem('alphatrion_org_id', orgId);
48-
}
49-
50-
// Step 2: Query user information
51-
const data = await graphqlQuery<{ user: User }>(
52-
queries.getUser,
53-
{ id: userId }
54-
);
55-
56-
if (!data.user) {
57-
throw new Error(`User with ID ${userId} not found`);
58-
}
103+
localStorage.setItem('alphatrion_org_id', orgId);
59104

60-
setCurrentUser(data.user);
105+
// Use stored user info (already complete from login)
106+
setCurrentUser(user);
61107

62-
// Step 3: Query user's teams and auto-select team
63-
const teamsData = await graphqlQuery<{ teams: Team[] }>(
64-
queries.listTeams
65-
);
66-
67-
if (teamsData.teams && teamsData.teams.length > 0) {
68-
// Check if this user has a saved team preference in localStorage
108+
// Handle team selection
109+
if (user.teams && user.teams.length > 0) {
110+
// Check for saved team preference in localStorage
69111
const teamKey = `alphatrion_selected_team_${userId}`;
70112
const savedTeamId = localStorage.getItem(teamKey);
71113

72-
let selectedTeam: Team;
114+
let selectedTeamId: string;
73115

74116
if (savedTeamId) {
75-
// Verify saved team still exists in user's teams
76-
const savedTeam = teamsData.teams.find(t => t.id === savedTeamId);
77-
if (savedTeam) {
78-
selectedTeam = savedTeam;
79-
} else {
80-
// Saved team not found, use first team
81-
selectedTeam = teamsData.teams[0];
82-
}
117+
const savedTeam = user.teams.find((t: any) => t.id === savedTeamId);
118+
selectedTeamId = savedTeam ? savedTeamId : user.teams[0].id;
83119
} else {
84120
// No saved team, use first team
85-
selectedTeam = teamsData.teams[0];
121+
selectedTeamId = user.teams[0].id;
86122
}
87123

88-
// Store team_id for UI (org_id already set from config)
89-
localStorage.setItem('alphatrion_team_id', selectedTeam.id);
90-
91-
setSelectedTeamId(selectedTeam.id, userId);
124+
localStorage.setItem('alphatrion_team_id', selectedTeamId);
125+
setSelectedTeamId(selectedTeamId, userId);
92126
}
93127
} catch (err) {
94128
console.error('Failed to initialize app:', err);
@@ -99,7 +133,7 @@ function App() {
99133
}
100134

101135
initialize();
102-
}, [setSelectedTeamId, queryClient]);
136+
}, [setSelectedTeamId, queryClient, navigate]);
103137

104138
if (loading) {
105139
return (
@@ -117,31 +151,28 @@ function App() {
117151
<div className="flex h-screen items-center justify-center">
118152
<div className="text-center max-w-md">
119153
<h1 className="text-2xl font-bold text-red-600 mb-4">
120-
Error Loading User
154+
Error Initializing Dashboard
121155
</h1>
122156
<p className="text-gray-700 mb-2">{error.message}</p>
123157
<p className="text-gray-500 text-sm">
124-
Please verify:
158+
Please try:
125159
</p>
126160
<ul className="text-gray-500 text-sm text-left mt-2 space-y-1">
127-
<li>The user ID exists in the database</li>
128-
<li>The backend server is running</li>
129-
<li>The dashboard was started with correct --user-id flag</li>
161+
<li>Clear browser cache and localStorage</li>
162+
<li>Verify the backend server is running</li>
163+
<li><button onClick={() => { localStorage.clear(); window.location.href = '/login'; }} className="text-blue-600 underline">Logout and login again</button></li>
130164
</ul>
131165
</div>
132166
</div>
133167
);
134168
}
135169

136-
if (!currentUser) {
137-
return null;
138-
}
139-
140170
return (
141171
<div className="h-full">
142-
<UserProvider user={currentUser}>
143-
<Routes>
144-
<Route path="/" element={<Layout />}>
172+
<Routes>
173+
<Route path="/login" element={<LoginPage />} />
174+
{currentUser ? (
175+
<Route path="/" element={<UserProvider user={currentUser}><Layout /></UserProvider>}>
145176
<Route index element={<DashboardPage />} />
146177
<Route path="experiments">
147178
<Route index element={<ExperimentsPage />} />
@@ -162,8 +193,8 @@ function App() {
162193
<Route path="datasets" element={<DatasetsPage />} />
163194
<Route path="artifacts" element={<ArtifactsPage />} />
164195
</Route>
165-
</Routes>
166-
</UserProvider>
196+
) : null}
197+
</Routes>
167198
</div>
168199
);
169200
}

0 commit comments

Comments
 (0)