@programinglive/dev-workflow-mcp-server 1.7.13 ā 1.7.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +69 -1
- package/cloudflared-config.yml +3 -0
- package/db/adapters/postgres.js +85 -28
- package/db/adapters/sqlite.js +7 -1
- package/docker-compose.yml +17 -0
- package/docs/product/PRD.md +4 -2
- package/docs/release-notes/RELEASE_NOTES.md +61 -0
- package/index.js +12 -2
- package/netlify.toml +7 -0
- package/nginx-api.conf +27 -0
- package/package.json +1 -1
- package/scripts/test-auth-flow.js +75 -0
- package/tests/build.test.js +1 -1
- package/tests/startup-resilience.test.js +73 -0
- package/web/Dockerfile.api +25 -0
- package/web/__tests__/api/workflow-history.test.js +14 -12
- package/web/api-server.js +211 -0
- package/web/app/docs/page.tsx +1 -1
- package/web/app/history/page.tsx +25 -18
- package/web/app/login/page.tsx +12 -14
- package/web/components/Hero.tsx +1 -1
- package/web/components/History.tsx +4 -4
- package/web/components/Navbar.tsx +11 -12
- package/web/lib/api-client.ts +123 -0
- package/web/netlify.toml +5 -0
- package/web/next.config.ts +2 -2
- package/web/package-lock.json +6407 -2241
- package/web/package.json +11 -2
- package/workflow-state.js +3 -2
- package/scripts/add-user.js +0 -52
- package/scripts/health-check.js +0 -50
- package/scripts/test-history.js +0 -56
- package/web/app/api/history-fallback/route.ts +0 -56
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
|
|
3
|
+
const API_BASE_URL = 'https://hollywood-andrew-cycle-genre.trycloudflare.com';
|
|
4
|
+
const USERNAME = 'programinglive';
|
|
5
|
+
const PASSWORD = 'mcpdevserver';
|
|
6
|
+
|
|
7
|
+
async function testAuthFlow() {
|
|
8
|
+
console.log('š Starting Auth Flow Test...');
|
|
9
|
+
console.log(`Target: ${API_BASE_URL}\n`);
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// 1. Login
|
|
13
|
+
console.log('1ļøā£ Attempting Login...');
|
|
14
|
+
const loginRes = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
body: JSON.stringify({ username: USERNAME, password: PASSWORD }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log(` Status: ${loginRes.status}`);
|
|
21
|
+
if (!loginRes.ok) {
|
|
22
|
+
throw new Error(`Login failed: ${loginRes.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const loginData = await loginRes.json();
|
|
26
|
+
const token = loginData.token;
|
|
27
|
+
|
|
28
|
+
if (token) {
|
|
29
|
+
console.log(' ā
Login Successful! JWT Token received.');
|
|
30
|
+
console.log(` Token: ${token.substring(0, 20)}...`);
|
|
31
|
+
} else {
|
|
32
|
+
console.log(' ā ļø Login successful but NO TOKEN returned (Legacy Session mode?)');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Access Protected Route (GET /me)
|
|
36
|
+
console.log('\n2ļøā£ Verifying Session/Token (GET /api/auth/me)...');
|
|
37
|
+
const headers = {};
|
|
38
|
+
if (token) {
|
|
39
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
40
|
+
console.log(' Using Authorization: Bearer header');
|
|
41
|
+
} else {
|
|
42
|
+
console.log(' Using Cookies (if any)');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const meRes = await fetch(`${API_BASE_URL}/api/auth/me`, {
|
|
46
|
+
headers,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
console.log(` Status: ${meRes.status}`);
|
|
50
|
+
if (meRes.ok) {
|
|
51
|
+
const meData = await meRes.json();
|
|
52
|
+
console.log(` ā
Authorized! User: ${meData.username} (${meData.email})`);
|
|
53
|
+
} else {
|
|
54
|
+
console.error(' ā Authorization Failed!');
|
|
55
|
+
console.error(await meRes.text());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Access History (GET /api/history)
|
|
59
|
+
console.log('\n3ļøā£ Fetching History...');
|
|
60
|
+
const historyRes = await fetch(`${API_BASE_URL}/api/history`, { headers });
|
|
61
|
+
console.log(` Status: ${historyRes.status}`);
|
|
62
|
+
|
|
63
|
+
if (historyRes.ok) {
|
|
64
|
+
const historyData = await historyRes.json();
|
|
65
|
+
console.log(` ā
History fetched! Count: ${historyData.history ? historyData.history.length : 0}`);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(' ā History fetch failed.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('\nā Test Failed:', error.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
testAuthFlow();
|
package/tests/build.test.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|
|
8
8
|
const exec = promisify(execCallback);
|
|
9
9
|
|
|
10
10
|
test('build generates dist/index.mjs file', async () => {
|
|
11
|
-
const { stderr } = await exec('npm run build:server');
|
|
11
|
+
const { stderr } = await exec('NODE_ENV=development npm run build:server');
|
|
12
12
|
// Allow the known vite warning about node:process
|
|
13
13
|
const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
|
|
14
14
|
const tailwindOutput = 'ā tailwindcss';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const ROOT = path.join(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
test('Server startup performance and resilience', async (t) => {
|
|
12
|
+
await t.test('Server starts in under 2 seconds (Studio timeout safety)', async () => {
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
const server = spawn('node', [path.join(ROOT, 'index.js')], {
|
|
15
|
+
env: { ...process.env, DEV_WORKFLOW_DB_TYPE: 'sqlite' },
|
|
16
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let output = '';
|
|
21
|
+
const timeout = setTimeout(() => {
|
|
22
|
+
server.kill();
|
|
23
|
+
reject(new Error('Server timed out starting'));
|
|
24
|
+
}, 5000);
|
|
25
|
+
|
|
26
|
+
server.stderr.on('data', (data) => {
|
|
27
|
+
output += data.toString();
|
|
28
|
+
if (output.includes('running on stdio')) {
|
|
29
|
+
const duration = Date.now() - start;
|
|
30
|
+
console.log(`Verified startup duration: ${duration}ms`);
|
|
31
|
+
assert.ok(duration < 2000, `Startup should be fast (took ${duration}ms)`);
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
server.kill();
|
|
34
|
+
resolve();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
server.on('error', (err) => {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
reject(err);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await t.test('Resilience: Server starts even with unreachable database', async () => {
|
|
46
|
+
const server = spawn('node', [path.join(ROOT, 'index.js')], {
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
DEV_WORKFLOW_DB_TYPE: 'postgres',
|
|
50
|
+
DEV_WORKFLOW_DB_URL: 'postgres://user:pass@1.2.3.4:5432/invalid'
|
|
51
|
+
},
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
let output = '';
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
server.kill();
|
|
59
|
+
reject(new Error('Server hung on unreachable DB'));
|
|
60
|
+
}, 15000); // Allow time for the 5s DB timeout + startup
|
|
61
|
+
|
|
62
|
+
server.stderr.on('data', (data) => {
|
|
63
|
+
output += data.toString();
|
|
64
|
+
if (output.includes('running on stdio')) {
|
|
65
|
+
assert.ok(true, 'Server started despite invalid DB');
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
server.kill();
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FROM node:20-alpine
|
|
2
|
+
|
|
3
|
+
# Install dependencies for native modules
|
|
4
|
+
RUN apk add --no-cache python3 make g++ postgresql-client
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# Copy package files
|
|
9
|
+
COPY package*.json ./
|
|
10
|
+
|
|
11
|
+
# Install dependencies
|
|
12
|
+
RUN npm install --only=production
|
|
13
|
+
|
|
14
|
+
# Copy application code
|
|
15
|
+
COPY . .
|
|
16
|
+
|
|
17
|
+
# Expose API port
|
|
18
|
+
EXPOSE 8080
|
|
19
|
+
|
|
20
|
+
# Set environment
|
|
21
|
+
ENV NODE_ENV=production
|
|
22
|
+
ENV API_PORT=8080
|
|
23
|
+
|
|
24
|
+
# Run API server
|
|
25
|
+
CMD ["node", "api-server.js"]
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import express from 'express';
|
|
3
5
|
|
|
4
6
|
describe('Workflow History API', () => {
|
|
5
7
|
let app;
|
|
6
8
|
let server;
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
test('setup app', () => {
|
|
9
11
|
// Mock Express app for testing
|
|
10
12
|
app = express();
|
|
11
13
|
app.use(express.json());
|
|
@@ -40,9 +42,9 @@ describe('Workflow History API', () => {
|
|
|
40
42
|
.get('/api/workflow/history')
|
|
41
43
|
.expect(200);
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
assert(response.body.history, 'History property should exist');
|
|
46
|
+
assert(Array.isArray(response.body.history), 'History should be an array');
|
|
47
|
+
assert(response.body.history.length > 0, 'History should not be empty');
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test('History items have required fields', async () => {
|
|
@@ -51,11 +53,11 @@ describe('Workflow History API', () => {
|
|
|
51
53
|
.expect(200);
|
|
52
54
|
|
|
53
55
|
const item = response.body.history[0];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
assert(item.id, 'Item should have id');
|
|
57
|
+
assert(item.task_type, 'Item should have task_type');
|
|
58
|
+
assert(item.description, 'Item should have description');
|
|
59
|
+
assert(item.commit_message, 'Item should have commit_message');
|
|
60
|
+
assert(item.completed_at, 'Item should have completed_at');
|
|
61
|
+
assert(item.tests_passed !== undefined, 'Item should have tests_passed');
|
|
60
62
|
});
|
|
61
63
|
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import bcrypt from 'bcrypt';
|
|
6
|
+
import jwt from 'jsonwebtoken';
|
|
7
|
+
|
|
8
|
+
dotenv.config({ path: '../.env' });
|
|
9
|
+
|
|
10
|
+
const app = express();
|
|
11
|
+
const { Pool } = pg;
|
|
12
|
+
|
|
13
|
+
const PORT = process.env.API_PORT || 8080;
|
|
14
|
+
const dbUrl = process.env.DEV_WORKFLOW_DB_URL;
|
|
15
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'dev-workflow-jwt-secret-change-me';
|
|
16
|
+
|
|
17
|
+
if (!dbUrl) {
|
|
18
|
+
console.error('ā DEV_WORKFLOW_DB_URL is not set');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pool = new Pool({ connectionString: dbUrl });
|
|
23
|
+
|
|
24
|
+
// Authentication helper functions
|
|
25
|
+
async function authenticateUser(pool, username, password) {
|
|
26
|
+
const result = await pool.query(
|
|
27
|
+
'SELECT * FROM users WHERE username = $1',
|
|
28
|
+
[username]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (result.rows.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const user = result.rows[0];
|
|
36
|
+
const isValid = await bcrypt.compare(password, user.password_hash);
|
|
37
|
+
|
|
38
|
+
if (!isValid) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: user.id,
|
|
44
|
+
username: user.username,
|
|
45
|
+
email: user.email,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getUserById(pool, userId) {
|
|
50
|
+
const result = await pool.query(
|
|
51
|
+
'SELECT id, username, email FROM users WHERE id = $1',
|
|
52
|
+
[userId]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return result.rows[0] || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// CORS configuration - allow Netlify domain
|
|
59
|
+
const corsOptions = {
|
|
60
|
+
origin: [
|
|
61
|
+
'https://devworkflow.programinglive.com',
|
|
62
|
+
'http://localhost:3000',
|
|
63
|
+
],
|
|
64
|
+
credentials: true,
|
|
65
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
66
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
app.use(cors(corsOptions));
|
|
70
|
+
app.use(express.json());
|
|
71
|
+
app.use(express.urlencoded({ extended: true }));
|
|
72
|
+
app.set('trust proxy', 1);
|
|
73
|
+
|
|
74
|
+
// Middleware to verify JWT
|
|
75
|
+
const requireAuth = (req, res, next) => {
|
|
76
|
+
const authHeader = req.headers.authorization;
|
|
77
|
+
|
|
78
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
79
|
+
const token = authHeader.split(' ')[1];
|
|
80
|
+
try {
|
|
81
|
+
const decoded = jwt.verify(token, JWT_SECRET);
|
|
82
|
+
req.user = decoded;
|
|
83
|
+
return next();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Health check endpoint
|
|
93
|
+
app.get('/api/health', (req, res) => {
|
|
94
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Authentication endpoints
|
|
98
|
+
app.post('/api/auth/login', async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const { username, password } = req.body;
|
|
101
|
+
|
|
102
|
+
if (!username || !password) {
|
|
103
|
+
return res.status(400).json({ error: 'Username and password are required' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const user = await authenticateUser(pool, username, password);
|
|
107
|
+
|
|
108
|
+
if (!user) {
|
|
109
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Generate JWT Token
|
|
113
|
+
const token = jwt.sign(
|
|
114
|
+
{ userId: user.id, username: user.username },
|
|
115
|
+
JWT_SECRET,
|
|
116
|
+
{ expiresIn: '30d' }
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
res.json({
|
|
120
|
+
success: true,
|
|
121
|
+
token,
|
|
122
|
+
user: {
|
|
123
|
+
id: user.id,
|
|
124
|
+
username: user.username,
|
|
125
|
+
email: user.email,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Login error:', error);
|
|
130
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
app.post('/api/auth/logout', (req, res) => {
|
|
135
|
+
// Client-side only action for JWT
|
|
136
|
+
res.json({ success: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.get('/api/auth/me', requireAuth, async (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
const user = await getUserById(pool, req.user.userId);
|
|
142
|
+
|
|
143
|
+
if (!user) {
|
|
144
|
+
return res.status(404).json({ error: 'User not found' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
res.json({
|
|
148
|
+
id: user.id,
|
|
149
|
+
username: user.username,
|
|
150
|
+
email: user.email,
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Get user error:', error);
|
|
154
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Workflow history endpoints
|
|
159
|
+
app.get('/api/history', requireAuth, async (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const result = await pool.query(
|
|
162
|
+
`SELECT id, task_type, description, commit_message, completed_at, tests_passed, documentation_type
|
|
163
|
+
FROM workflow_history
|
|
164
|
+
WHERE user_id = $1
|
|
165
|
+
ORDER BY completed_at DESC
|
|
166
|
+
LIMIT 100`,
|
|
167
|
+
[req.user.userId]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
res.json({
|
|
171
|
+
history: result.rows,
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Get history error:', error);
|
|
175
|
+
res.status(500).json({ error: 'Failed to fetch history' });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
app.post('/api/history', requireAuth, async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const { task_type, description, commit_message, tests_passed, documentation_type } = req.body;
|
|
182
|
+
|
|
183
|
+
const result = await pool.query(
|
|
184
|
+
`INSERT INTO workflow_history (user_id, task_type, description, commit_message, tests_passed, documentation_type, completed_at)
|
|
185
|
+
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
|
186
|
+
RETURNING *`,
|
|
187
|
+
[req.user.userId, task_type, description, commit_message, tests_passed, documentation_type]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
res.json({
|
|
191
|
+
success: true,
|
|
192
|
+
history: result.rows[0],
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Create history error:', error);
|
|
196
|
+
res.status(500).json({ error: 'Failed to create history entry' });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Error handling middleware
|
|
201
|
+
app.use((err, req, res, next) => {
|
|
202
|
+
console.error('Unhandled error:', err);
|
|
203
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Start server
|
|
207
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
208
|
+
console.log(`ā
API server running on port ${PORT}`);
|
|
209
|
+
console.log(`š Database: ${dbUrl.replace(/:[^:]*@/, ':****@')}`);
|
|
210
|
+
console.log(`š CORS enabled for: ${corsOptions.origin.join(', ')}`);
|
|
211
|
+
});
|
package/web/app/docs/page.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export default function DocsPage() {
|
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
24
|
<div className="bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-8">
|
|
25
|
-
<p className="text-emerald-700 dark:text-emerald-400 font-semibold">Version: 1.7.
|
|
25
|
+
<p className="text-emerald-700 dark:text-emerald-400 font-semibold">Version: 1.7.15</p>
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
28
|
<h2 className="text-3xl font-bold mt-12 mb-6 text-gray-900 dark:text-white">Features</h2>
|
package/web/app/history/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useMemo } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { apiClient } from '@/lib/api-client';
|
|
5
6
|
import {
|
|
6
7
|
CheckCircle,
|
|
7
8
|
XCircle,
|
|
@@ -34,27 +35,33 @@ export default function HistoryPage() {
|
|
|
34
35
|
const router = useRouter();
|
|
35
36
|
|
|
36
37
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
localStorage.setItem('isLoggedIn', 'true');
|
|
43
|
-
window.dispatchEvent(new Event('authUpdate'));
|
|
44
|
-
return fetch('/api/history-fallback');
|
|
45
|
-
} else {
|
|
46
|
-
router.push('/login');
|
|
38
|
+
const loadData = async () => {
|
|
39
|
+
try {
|
|
40
|
+
// Verify auth
|
|
41
|
+
const user = await apiClient.getCurrentUser();
|
|
42
|
+
if (!user) {
|
|
47
43
|
throw new Error('Not authenticated');
|
|
48
44
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
|
|
46
|
+
setIsAuthenticated(true);
|
|
47
|
+
// Sync auth state
|
|
48
|
+
localStorage.setItem('isLoggedIn', 'true');
|
|
49
|
+
window.dispatchEvent(new Event('authUpdate'));
|
|
50
|
+
|
|
51
|
+
// Fetch history
|
|
52
|
+
const result = await apiClient.getHistory();
|
|
53
|
+
if (result?.history) {
|
|
54
|
+
setHistory(result.history);
|
|
54
55
|
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('History load error:', error);
|
|
58
|
+
router.push('/login');
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
loadData();
|
|
58
65
|
}, [router]);
|
|
59
66
|
|
|
60
67
|
const stats = useMemo(() => {
|
package/web/app/login/page.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
5
|
import Image from 'next/image';
|
|
6
6
|
import { Lock, User } from 'lucide-react';
|
|
7
|
+
import { apiClient } from '@/lib/api-client';
|
|
7
8
|
|
|
8
9
|
export default function LoginPage() {
|
|
9
10
|
const [username, setUsername] = useState('');
|
|
@@ -21,9 +22,9 @@ export default function LoginPage() {
|
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
.then(
|
|
26
|
-
if (
|
|
25
|
+
apiClient.getCurrentUser()
|
|
26
|
+
.then(user => {
|
|
27
|
+
if (user) {
|
|
27
28
|
router.push('/history');
|
|
28
29
|
}
|
|
29
30
|
})
|
|
@@ -37,23 +38,17 @@ export default function LoginPage() {
|
|
|
37
38
|
setLoading(true);
|
|
38
39
|
|
|
39
40
|
try {
|
|
40
|
-
const
|
|
41
|
-
method: 'POST',
|
|
42
|
-
headers: { 'Content-Type': 'application/json' },
|
|
43
|
-
body: JSON.stringify({ username, password }),
|
|
44
|
-
});
|
|
41
|
+
const data = await apiClient.login(username, password);
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (response.ok) {
|
|
43
|
+
if (data.success) {
|
|
49
44
|
localStorage.setItem('isLoggedIn', 'true');
|
|
50
45
|
window.dispatchEvent(new Event('authUpdate'));
|
|
51
46
|
router.push('/history');
|
|
52
47
|
} else {
|
|
53
|
-
setError(
|
|
48
|
+
setError('Login failed');
|
|
54
49
|
}
|
|
55
|
-
} catch (err) {
|
|
56
|
-
setError('Network error. Please try again.');
|
|
50
|
+
} catch (err: any) {
|
|
51
|
+
setError(err.message || 'Network error. Please try again.');
|
|
57
52
|
} finally {
|
|
58
53
|
setLoading(false);
|
|
59
54
|
}
|
|
@@ -77,6 +72,9 @@ export default function LoginPage() {
|
|
|
77
72
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
78
73
|
Sign in to access your workflow history
|
|
79
74
|
</p>
|
|
75
|
+
<p className="mt-1 text-xs text-gray-400 font-mono">
|
|
76
|
+
v1.7.15
|
|
77
|
+
</p>
|
|
80
78
|
</div>
|
|
81
79
|
|
|
82
80
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
package/web/components/Hero.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
+
import { apiClient } from "@/lib/api-client";
|
|
4
5
|
|
|
5
6
|
type HistoryItem = {
|
|
6
7
|
id: number;
|
|
@@ -31,11 +32,10 @@ export default function History() {
|
|
|
31
32
|
|
|
32
33
|
const loadData = async () => {
|
|
33
34
|
try {
|
|
34
|
-
// Fetch workflow history from API
|
|
35
|
-
const
|
|
35
|
+
// Fetch workflow history from API via apiClient
|
|
36
|
+
const data = await apiClient.getHistory();
|
|
36
37
|
|
|
37
|
-
if (
|
|
38
|
-
const data = await response.json();
|
|
38
|
+
if (data?.history) {
|
|
39
39
|
setHistory(data.history || []);
|
|
40
40
|
setTotalPages(Math.ceil((data.history?.length || 0) / 10));
|
|
41
41
|
setSummary({
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { Github, Menu, X, Moon, Sun, LogOut } from "lucide-react";
|
|
6
6
|
import Image from "next/image";
|
|
7
|
+
import { apiClient } from "@/lib/api-client";
|
|
7
8
|
|
|
8
9
|
export default function Navbar() {
|
|
9
10
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -29,18 +30,16 @@ export default function Navbar() {
|
|
|
29
30
|
const mightBeLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
|
30
31
|
|
|
31
32
|
if (mightBeLoggedIn) {
|
|
32
|
-
|
|
33
|
-
.then(res => {
|
|
34
|
-
if (res.ok) {
|
|
35
|
-
return res.json();
|
|
36
|
-
}
|
|
37
|
-
throw new Error('Not authenticated');
|
|
38
|
-
})
|
|
33
|
+
apiClient.getCurrentUser()
|
|
39
34
|
.then(data => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
if (data) {
|
|
36
|
+
setIsAuthenticated(true);
|
|
37
|
+
setUsername(data.username || 'User');
|
|
38
|
+
// Ensure local storage is in sync
|
|
39
|
+
localStorage.setItem('isLoggedIn', 'true');
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error('Not authenticated');
|
|
42
|
+
}
|
|
44
43
|
})
|
|
45
44
|
.catch(() => {
|
|
46
45
|
// Session invalid/expired
|
|
@@ -74,7 +73,7 @@ export default function Navbar() {
|
|
|
74
73
|
|
|
75
74
|
const handleLogout = async () => {
|
|
76
75
|
try {
|
|
77
|
-
await
|
|
76
|
+
await apiClient.logout();
|
|
78
77
|
localStorage.removeItem('isLoggedIn');
|
|
79
78
|
setIsAuthenticated(false);
|
|
80
79
|
setUsername(null);
|