@programinglive/dev-workflow-mcp-server 1.7.10 → 1.7.12
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/.dockerignore +60 -0
- package/.env.example +12 -0
- package/.env.production +16 -0
- package/CHANGELOG.md +45 -0
- package/Dockerfile +32 -0
- package/README.md +63 -2
- package/db/adapters/mysql.js +11 -4
- package/db/migrations/002_create_users_table.sql +22 -0
- package/db/migrations/003_create_workflow_history.sql +14 -0
- package/db/migrations/004_fix_workflow_history.sql +3 -0
- package/docker-compose.yml +24 -0
- package/docs/GCP_DEPLOYMENT.md +298 -0
- package/docs/product/PRD.md +5 -1
- package/docs/release-notes/RELEASE_NOTES.md +26 -0
- package/index.js +2 -0
- package/package.json +9 -3
- package/scripts/add-user.js +52 -0
- package/scripts/create-admin.js +47 -0
- package/scripts/deploy-gcloud.ps1 +160 -0
- package/scripts/deploy-gcloud.sh +154 -0
- package/tests/db-adapters.test.js +108 -0
- package/tests/env-support.test.js +46 -0
- package/tests/workflowState.test.js +16 -1
- package/web/__tests__/api/workflow-history.test.js +61 -0
- package/web/app/docs/page.tsx +1 -1
- package/web/app/history/page.tsx +213 -0
- package/web/app/layout.tsx +1 -1
- package/web/app/login/page.tsx +144 -0
- package/web/app/page.tsx +0 -2
- package/web/components/Hero.tsx +1 -1
- package/web/components/History.tsx +63 -16
- package/web/components/Navbar.tsx +111 -9
- package/web/lib/auth.js +75 -0
- package/web/middleware/auth.js +26 -0
- package/web/next.config.ts +4 -0
- package/web/package-lock.json +40 -40
- package/web/package.json +6 -5
- package/web/scripts/smoke-test.js +30 -0
- package/web/server.js +163 -18
|
@@ -14,6 +14,13 @@ import { constants as fsConstants } from "node:fs";
|
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
// Isolate these tests from the real database configuration
|
|
19
|
+
delete process.env.DEV_WORKFLOW_DB_TYPE;
|
|
20
|
+
delete process.env.DEV_WORKFLOW_DB_URL;
|
|
21
|
+
delete process.env.TEST_MYSQL_URL;
|
|
22
|
+
delete process.env.TEST_POSTGRES_URL;
|
|
23
|
+
|
|
17
24
|
import {
|
|
18
25
|
WorkflowState,
|
|
19
26
|
getNextStep,
|
|
@@ -50,7 +57,9 @@ test("resolveStateFile prefers current working directory when multiple project m
|
|
|
50
57
|
const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
|
|
51
58
|
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
52
59
|
|
|
53
|
-
await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module" }));
|
|
60
|
+
await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module", type: "module" }));
|
|
61
|
+
await mkdir(path.join(moduleDir, "db"), { recursive: true });
|
|
62
|
+
await writeFile(path.join(moduleDir, "db", "index.js"), 'export function getDbAdapter() { return { connect: async () => {}, saveState: async () => {}, getState: async () => null }; } export async function insertHistoryEntry() {} export async function updateSummaryForUser() {} export async function saveState() {} export async function getState() {} export async function getSummaryForUser() { return { totalTasks: 0, taskTypes: {} }; } export async function getHistoryForUser() { return { entries: [], total: 0 }; }');
|
|
54
63
|
await writeFile(path.join(projectA, "package.json"), JSON.stringify({ name: "project-a" }));
|
|
55
64
|
await writeFile(path.join(projectB, "package.json"), JSON.stringify({ name: "project-b" }));
|
|
56
65
|
|
|
@@ -101,6 +110,9 @@ test("WorkflowState mirrors state file to compatibility locations", async () =>
|
|
|
101
110
|
await mkdir(path.join(tempRoot, "dist"), { recursive: true });
|
|
102
111
|
await mkdir(stateDir, { recursive: true });
|
|
103
112
|
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
113
|
+
await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module", type: "module" }));
|
|
114
|
+
await mkdir(path.join(moduleDir, "db"), { recursive: true });
|
|
115
|
+
await writeFile(path.join(moduleDir, "db", "index.js"), 'export function getDbAdapter() { return { connect: async () => {}, saveState: async () => {}, getState: async () => null }; } export async function insertHistoryEntry() {} export async function updateSummaryForUser() {} export async function saveState() {} export async function getState() {} export async function getSummaryForUser() { return { totalTasks: 0, taskTypes: {} }; } export async function getHistoryForUser() { return { entries: [], total: 0 }; }');
|
|
104
116
|
await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
|
|
105
117
|
|
|
106
118
|
const stateFile = path.join(stateDir, "workflow-state.json");
|
|
@@ -129,6 +141,9 @@ test("WorkflowState avoids filesystem root when no project markers found", async
|
|
|
129
141
|
|
|
130
142
|
const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
|
|
131
143
|
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
144
|
+
await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module", type: "module" }));
|
|
145
|
+
await mkdir(path.join(moduleDir, "db"), { recursive: true });
|
|
146
|
+
await writeFile(path.join(moduleDir, "db", "index.js"), 'export function getDbAdapter() { return { connect: async () => {}, saveState: async () => {}, getState: async () => null }; } export async function insertHistoryEntry() {} export async function updateSummaryForUser() {} export async function saveState() {} export async function getState() {} export async function getSummaryForUser() { return { totalTasks: 0, taskTypes: {} }; } export async function getHistoryForUser() { return { entries: [], total: 0 }; }');
|
|
132
147
|
|
|
133
148
|
const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
|
|
134
149
|
const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const request = require('supertest');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
|
|
4
|
+
describe('Workflow History API', () => {
|
|
5
|
+
let app;
|
|
6
|
+
let server;
|
|
7
|
+
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
// Mock Express app for testing
|
|
10
|
+
app = express();
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
|
|
13
|
+
// Mock session middleware
|
|
14
|
+
app.use((req, res, next) => {
|
|
15
|
+
req.session = { userId: 1 };
|
|
16
|
+
next();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Mock history endpoint
|
|
20
|
+
app.get('/api/workflow/history', async (req, res) => {
|
|
21
|
+
// Mock response
|
|
22
|
+
res.json({
|
|
23
|
+
history: [
|
|
24
|
+
{
|
|
25
|
+
id: 1,
|
|
26
|
+
task_type: 'feature',
|
|
27
|
+
description: 'Test feature',
|
|
28
|
+
commit_message: 'feat: test commit',
|
|
29
|
+
completed_at: new Date().toISOString(),
|
|
30
|
+
tests_passed: true,
|
|
31
|
+
documentation_type: 'README'
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('GET /api/workflow/history returns workflow history', async () => {
|
|
39
|
+
const response = await request(app)
|
|
40
|
+
.get('/api/workflow/history')
|
|
41
|
+
.expect(200);
|
|
42
|
+
|
|
43
|
+
expect(response.body).toHaveProperty('history');
|
|
44
|
+
expect(Array.isArray(response.body.history)).toBe(true);
|
|
45
|
+
expect(response.body.history.length).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('History items have required fields', async () => {
|
|
49
|
+
const response = await request(app)
|
|
50
|
+
.get('/api/workflow/history')
|
|
51
|
+
.expect(200);
|
|
52
|
+
|
|
53
|
+
const item = response.body.history[0];
|
|
54
|
+
expect(item).toHaveProperty('id');
|
|
55
|
+
expect(item).toHaveProperty('task_type');
|
|
56
|
+
expect(item).toHaveProperty('description');
|
|
57
|
+
expect(item).toHaveProperty('commit_message');
|
|
58
|
+
expect(item).toHaveProperty('completed_at');
|
|
59
|
+
expect(item).toHaveProperty('tests_passed');
|
|
60
|
+
});
|
|
61
|
+
});
|
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.
|
|
25
|
+
<p className="text-emerald-700 dark:text-emerald-400 font-semibold">Version: 1.7.11</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>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import {
|
|
6
|
+
CheckCircle,
|
|
7
|
+
XCircle,
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
GitCommit,
|
|
10
|
+
FileText,
|
|
11
|
+
Activity,
|
|
12
|
+
Search,
|
|
13
|
+
Filter,
|
|
14
|
+
Calendar,
|
|
15
|
+
BarChart3
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
interface WorkflowHistoryItem {
|
|
19
|
+
id: number;
|
|
20
|
+
task_type: string;
|
|
21
|
+
description: string;
|
|
22
|
+
commit_message: string;
|
|
23
|
+
completed_at: string;
|
|
24
|
+
tests_passed: boolean;
|
|
25
|
+
documentation_type: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function HistoryPage() {
|
|
29
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
30
|
+
const [loading, setLoading] = useState(true);
|
|
31
|
+
const [history, setHistory] = useState<WorkflowHistoryItem[]>([]);
|
|
32
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
33
|
+
const [typeFilter, setTypeFilter] = useState<string>('all');
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetch('/api/auth/me')
|
|
38
|
+
.then(res => {
|
|
39
|
+
if (res.ok) {
|
|
40
|
+
setIsAuthenticated(true);
|
|
41
|
+
// Sync auth state with Navbar
|
|
42
|
+
localStorage.setItem('isLoggedIn', 'true');
|
|
43
|
+
window.dispatchEvent(new Event('authUpdate'));
|
|
44
|
+
return fetch('/api/workflow/history');
|
|
45
|
+
} else {
|
|
46
|
+
router.push('/login');
|
|
47
|
+
throw new Error('Not authenticated');
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.then(res => res?.json())
|
|
51
|
+
.then(data => {
|
|
52
|
+
if (data?.history) {
|
|
53
|
+
setHistory(data.history);
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.catch(() => { })
|
|
57
|
+
.finally(() => setLoading(false));
|
|
58
|
+
}, [router]);
|
|
59
|
+
|
|
60
|
+
const stats = useMemo(() => {
|
|
61
|
+
const total = history.length;
|
|
62
|
+
const features = history.filter(h => h.task_type === 'feature').length;
|
|
63
|
+
const bugs = history.filter(h => h.task_type === 'bugfix').length;
|
|
64
|
+
const passedTests = history.filter(h => h.tests_passed).length;
|
|
65
|
+
const successRate = total > 0 ? Math.round((passedTests / total) * 100) : 0;
|
|
66
|
+
|
|
67
|
+
return { total, features, bugs, successRate };
|
|
68
|
+
}, [history]);
|
|
69
|
+
|
|
70
|
+
const filteredHistory = useMemo(() => {
|
|
71
|
+
return history.filter(item => {
|
|
72
|
+
const matchesSearch = item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
73
|
+
item.commit_message.toLowerCase().includes(searchQuery.toLowerCase());
|
|
74
|
+
const matchesType = typeFilter === 'all' || item.task_type === typeFilter;
|
|
75
|
+
return matchesSearch && matchesType;
|
|
76
|
+
});
|
|
77
|
+
}, [history, searchQuery, typeFilter]);
|
|
78
|
+
|
|
79
|
+
if (loading) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-[#0a0a0f]">
|
|
82
|
+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isAuthenticated) return null;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="min-h-screen pt-24 pb-12 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-[#0a0a0f] transition-colors duration-300">
|
|
91
|
+
<div className="max-w-7xl mx-auto space-y-8">
|
|
92
|
+
{/* Header */}
|
|
93
|
+
<div>
|
|
94
|
+
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-2">Workflow Insights</h1>
|
|
95
|
+
<p className="text-gray-600 dark:text-gray-400">Track and analyze your development velocity and quality.</p>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Stats Grid */}
|
|
99
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
100
|
+
<StatCard icon={<Activity className="w-5 h-5 text-indigo-500" />} label="Total Tasks" value={stats.total} />
|
|
101
|
+
<StatCard icon={<CheckCircle className="w-5 h-5 text-green-500" />} label="Success Rate" value={`${stats.successRate}%`} />
|
|
102
|
+
<StatCard icon={<BarChart3 className="w-5 h-5 text-blue-500" />} label="Features Built" value={stats.features} />
|
|
103
|
+
<StatCard icon={<AlertTriangle className="w-5 h-5 text-red-500" />} label="Bugs Fixed" value={stats.bugs} />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Controls */}
|
|
107
|
+
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between bg-white dark:bg-white/5 p-4 rounded-xl border border-gray-200 dark:border-white/10">
|
|
108
|
+
<div className="relative w-full sm:w-96">
|
|
109
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
110
|
+
<input
|
|
111
|
+
type="text"
|
|
112
|
+
placeholder="Search tasks, commits..."
|
|
113
|
+
value={searchQuery}
|
|
114
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
115
|
+
className="w-full pl-10 pr-4 py-2 bg-gray-100 dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-900 dark:text-white placeholder-gray-500"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex gap-2 w-full sm:w-auto">
|
|
119
|
+
<select
|
|
120
|
+
value={typeFilter}
|
|
121
|
+
onChange={(e) => setTypeFilter(e.target.value)}
|
|
122
|
+
className="px-4 py-2 bg-gray-100 dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-900 dark:text-white"
|
|
123
|
+
>
|
|
124
|
+
<option value="all">All Types</option>
|
|
125
|
+
<option value="feature">Features</option>
|
|
126
|
+
<option value="bugfix">Bug Fixes</option>
|
|
127
|
+
<option value="refactor">Refactors</option>
|
|
128
|
+
</select>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Timeline */}
|
|
133
|
+
<div className="space-y-6 relative before:absolute before:inset-0 before:ml-5 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-gray-300 before:dark:via-white/20 before:to-transparent">
|
|
134
|
+
{filteredHistory.length === 0 ? (
|
|
135
|
+
<div className="text-center py-12">
|
|
136
|
+
<p className="text-gray-500 dark:text-gray-400">No workflow history found.</p>
|
|
137
|
+
</div>
|
|
138
|
+
) : (
|
|
139
|
+
filteredHistory.map((item, index) => (
|
|
140
|
+
<div key={item.id} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group is-active">
|
|
141
|
+
{/* Icon */}
|
|
142
|
+
<div className="flex items-center justify-center w-10 h-10 rounded-full border border-white dark:border-[#0a0a0f] bg-gray-100 dark:bg-white/10 shadow shrink-0 md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 z-10">
|
|
143
|
+
{getTaskIcon(item.task_type)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Card */}
|
|
147
|
+
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] bg-white dark:bg-white/5 p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm hover:shadow-md transition-all duration-300">
|
|
148
|
+
<div className="flex items-center justify-between mb-3">
|
|
149
|
+
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium uppercase tracking-wide ${getTypeColor(item.task_type)}`}>
|
|
150
|
+
{item.task_type}
|
|
151
|
+
</span>
|
|
152
|
+
<time className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
|
153
|
+
{new Date(item.completed_at).toLocaleDateString()}
|
|
154
|
+
</time>
|
|
155
|
+
</div>
|
|
156
|
+
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2">{item.description}</h3>
|
|
157
|
+
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-4 bg-gray-50 dark:bg-black/20 p-2 rounded-lg break-all font-mono">
|
|
158
|
+
<GitCommit className="w-4 h-4 shrink-0" />
|
|
159
|
+
{item.commit_message}
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex flex-wrap items-center gap-3 pt-3 border-t border-gray-100 dark:border-white/10">
|
|
162
|
+
<div className={`flex items-center gap-1.5 text-xs font-medium ${item.tests_passed ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'}`}>
|
|
163
|
+
{item.tests_passed ? <CheckCircle className="w-3.5 h-3.5" /> : <AlertTriangle className="w-3.5 h-3.5" />}
|
|
164
|
+
{item.tests_passed ? 'Tests Passed' : 'Tests Skipped'}
|
|
165
|
+
</div>
|
|
166
|
+
{item.documentation_type && (
|
|
167
|
+
<div className="flex items-center gap-1.5 text-xs font-medium text-blue-600 dark:text-blue-400">
|
|
168
|
+
<FileText className="w-3.5 h-3.5" />
|
|
169
|
+
{item.documentation_type}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
))
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function StatCard({ icon, label, value }: { icon: React.ReactNode, label: string, value: string | number }) {
|
|
184
|
+
return (
|
|
185
|
+
<div className="bg-white dark:bg-white/5 p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm">
|
|
186
|
+
<div className="flex items-center gap-4">
|
|
187
|
+
<div className="p-3 bg-gray-50 dark:bg-white/5 rounded-lg">
|
|
188
|
+
{icon}
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{label}</p>
|
|
192
|
+
<p className="text-2xl font-bold text-gray-900 dark:text-white">{value}</p>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getTaskIcon(type: string) {
|
|
200
|
+
switch (type) {
|
|
201
|
+
case 'feature': return <CheckCircle className="w-5 h-5 text-blue-500" />;
|
|
202
|
+
case 'bugfix': return <XCircle className="w-5 h-5 text-red-500" />;
|
|
203
|
+
default: return <Activity className="w-5 h-5 text-gray-500" />;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getTypeColor(type: string) {
|
|
208
|
+
switch (type) {
|
|
209
|
+
case 'feature': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300';
|
|
210
|
+
case 'bugfix': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
|
|
211
|
+
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800/50 dark:text-gray-300';
|
|
212
|
+
}
|
|
213
|
+
}
|
package/web/app/layout.tsx
CHANGED
|
@@ -26,7 +26,7 @@ export default function RootLayout({
|
|
|
26
26
|
children: React.ReactNode;
|
|
27
27
|
}>) {
|
|
28
28
|
return (
|
|
29
|
-
<html lang="en" className="scroll-smooth">
|
|
29
|
+
<html lang="en" className="scroll-smooth" data-scroll-behavior="smooth">
|
|
30
30
|
<body className="antialiased bg-white dark:bg-[#0a0a0f] text-gray-900 dark:text-white transition-colors duration-300">
|
|
31
31
|
<ThemeProvider>
|
|
32
32
|
<Navbar />
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Image from 'next/image';
|
|
6
|
+
import { Lock, User } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export default function LoginPage() {
|
|
9
|
+
const [username, setUsername] = useState('');
|
|
10
|
+
const [password, setPassword] = useState('');
|
|
11
|
+
const [error, setError] = useState('');
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const [authChecking, setAuthChecking] = useState(true);
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// Check if already logged in
|
|
18
|
+
const mightBeLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
|
19
|
+
if (!mightBeLoggedIn) {
|
|
20
|
+
setAuthChecking(false);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fetch('/api/auth/me')
|
|
25
|
+
.then(res => {
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
router.push('/history');
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.catch(() => { })
|
|
31
|
+
.finally(() => setAuthChecking(false));
|
|
32
|
+
}, [router]);
|
|
33
|
+
|
|
34
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setError('');
|
|
37
|
+
setLoading(true);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch('/api/auth/login', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ username, password }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
localStorage.setItem('isLoggedIn', 'true');
|
|
50
|
+
window.dispatchEvent(new Event('authUpdate'));
|
|
51
|
+
router.push('/history');
|
|
52
|
+
} else {
|
|
53
|
+
setError(data.error || 'Login failed');
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
setError('Network error. Please try again.');
|
|
57
|
+
} finally {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (authChecking) {
|
|
63
|
+
return null; // Or a loading spinner
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-[#0a0a0f] px-4 transition-colors duration-300">
|
|
68
|
+
<div className="max-w-md w-full space-y-8 bg-white dark:bg-white/5 p-8 rounded-2xl shadow-xl border border-gray-100 dark:border-white/10">
|
|
69
|
+
<div className="text-center">
|
|
70
|
+
<div className="mx-auto h-12 w-12 relative mb-4">
|
|
71
|
+
<div className="absolute inset-0 bg-blue-500 rounded-xl blur opacity-20 dark:opacity-40"></div>
|
|
72
|
+
<Image src="/assets/logo.png" alt="Logo" width={48} height={48} className="relative rounded-xl" />
|
|
73
|
+
</div>
|
|
74
|
+
<h2 className="text-3xl font-bold text-gray-900 dark:text-white tracking-tight">
|
|
75
|
+
Welcome Back
|
|
76
|
+
</h2>
|
|
77
|
+
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
78
|
+
Sign in to access your workflow history
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
83
|
+
{error && (
|
|
84
|
+
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 border border-red-200 dark:border-red-900/50">
|
|
85
|
+
<p className="text-sm text-red-600 dark:text-red-400 text-center font-medium">{error}</p>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<div className="space-y-4">
|
|
90
|
+
<div className="relative">
|
|
91
|
+
<label htmlFor="username" className="sr-only">Username</label>
|
|
92
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
93
|
+
<User className="h-5 w-5 text-gray-400" />
|
|
94
|
+
</div>
|
|
95
|
+
<input
|
|
96
|
+
id="username"
|
|
97
|
+
name="username"
|
|
98
|
+
type="text"
|
|
99
|
+
required
|
|
100
|
+
className="appearance-none block w-full pl-10 pr-3 py-3 border border-gray-300 dark:border-white/10 rounded-xl placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-black/20 text-gray-900 dark:text-white sm:text-sm transition-all"
|
|
101
|
+
placeholder="Username"
|
|
102
|
+
value={username}
|
|
103
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
104
|
+
disabled={loading}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="relative">
|
|
108
|
+
<label htmlFor="password" className="sr-only">Password</label>
|
|
109
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
110
|
+
<Lock className="h-5 w-5 text-gray-400" />
|
|
111
|
+
</div>
|
|
112
|
+
<input
|
|
113
|
+
id="password"
|
|
114
|
+
name="password"
|
|
115
|
+
type="password"
|
|
116
|
+
required
|
|
117
|
+
className="appearance-none block w-full pl-10 pr-3 py-3 border border-gray-300 dark:border-white/10 rounded-xl placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-black/20 text-gray-900 dark:text-white sm:text-sm transition-all"
|
|
118
|
+
placeholder="Password"
|
|
119
|
+
value={password}
|
|
120
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
121
|
+
disabled={loading}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div>
|
|
127
|
+
<button
|
|
128
|
+
type="submit"
|
|
129
|
+
disabled={loading}
|
|
130
|
+
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg shadow-blue-500/30 hover:shadow-blue-500/40"
|
|
131
|
+
>
|
|
132
|
+
{loading ? (
|
|
133
|
+
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
134
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
135
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
136
|
+
</svg>
|
|
137
|
+
) : 'Sign in'}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</form>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
package/web/app/page.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Hero from "@/components/Hero";
|
|
2
2
|
import Features from "@/components/Features";
|
|
3
|
-
import History from "@/components/History";
|
|
4
3
|
import ApiReference from "@/components/ApiReference";
|
|
5
4
|
|
|
6
5
|
export default function Home() {
|
|
@@ -8,7 +7,6 @@ export default function Home() {
|
|
|
8
7
|
<main>
|
|
9
8
|
<Hero />
|
|
10
9
|
<Features />
|
|
11
|
-
<History />
|
|
12
10
|
<ApiReference />
|
|
13
11
|
</main>
|
|
14
12
|
);
|
package/web/components/Hero.tsx
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
4
|
|
|
5
5
|
type HistoryItem = {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
id: number;
|
|
7
|
+
task_type: string;
|
|
8
|
+
description: string;
|
|
9
|
+
commit_message: string;
|
|
10
|
+
completed_at: string;
|
|
11
|
+
tests_passed: boolean;
|
|
12
|
+
documentation_type: string;
|
|
8
13
|
};
|
|
9
14
|
|
|
10
15
|
type SummaryData = {
|
|
@@ -25,19 +30,47 @@ export default function History() {
|
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
const loadData = async () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
try {
|
|
34
|
+
// Fetch workflow history from API
|
|
35
|
+
const response = await fetch('/api/workflow/history');
|
|
36
|
+
|
|
37
|
+
if (response.ok) {
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
setHistory(data.history || []);
|
|
40
|
+
setTotalPages(Math.ceil((data.history?.length || 0) / 10));
|
|
41
|
+
setSummary({
|
|
42
|
+
summary: {
|
|
43
|
+
totalTasks: data.history?.length || 0,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
// Not authenticated or error - show empty state
|
|
48
|
+
setHistory([]);
|
|
49
|
+
setTotalPages(1);
|
|
50
|
+
setSummary({
|
|
51
|
+
summary: {
|
|
52
|
+
totalTasks: 0,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Silently handle errors - show empty state
|
|
58
|
+
setHistory([]);
|
|
59
|
+
setTotalPages(1);
|
|
60
|
+
setSummary({
|
|
61
|
+
summary: {
|
|
62
|
+
totalTasks: 0,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
37
66
|
};
|
|
38
67
|
|
|
39
68
|
useEffect(() => {
|
|
40
|
-
|
|
69
|
+
// Only load if we're likely authenticated (reduces 401 errors)
|
|
70
|
+
// History component should only be on authenticated pages anyway
|
|
71
|
+
if (typeof window !== 'undefined' && window.location.pathname === '/history') {
|
|
72
|
+
loadData();
|
|
73
|
+
}
|
|
41
74
|
}, [page]); // Reload when page changes
|
|
42
75
|
|
|
43
76
|
const handleApplyFilters = () => {
|
|
@@ -92,10 +125,24 @@ export default function History() {
|
|
|
92
125
|
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Recent History</h3>
|
|
93
126
|
<div className="space-y-3">
|
|
94
127
|
{history.length > 0 ? (
|
|
95
|
-
history.map((row
|
|
96
|
-
<div key={
|
|
97
|
-
<div className="
|
|
98
|
-
|
|
128
|
+
history.map((row) => (
|
|
129
|
+
<div key={row.id} className="p-4 rounded-lg bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10">
|
|
130
|
+
<div className="flex items-center gap-2 mb-2">
|
|
131
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${row.task_type === 'feature' ? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200' :
|
|
132
|
+
row.task_type === 'bugfix' ? 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200' :
|
|
133
|
+
'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200'
|
|
134
|
+
}`}>
|
|
135
|
+
{row.task_type}
|
|
136
|
+
</span>
|
|
137
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${row.tests_passed ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' :
|
|
138
|
+
'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200'
|
|
139
|
+
}`}>
|
|
140
|
+
{row.tests_passed ? '✓ Tests' : '⚠ Skipped'}
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="font-semibold text-gray-900 dark:text-white">{row.description}</div>
|
|
144
|
+
<div className="text-sm text-gray-700 dark:text-gray-300 mt-1">{row.commit_message}</div>
|
|
145
|
+
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">{new Date(row.completed_at).toLocaleString()}</div>
|
|
99
146
|
</div>
|
|
100
147
|
))
|
|
101
148
|
) : (
|