@programinglive/dev-workflow-mcp-server 1.7.11 → 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 +1 -0
- package/.env.production +16 -0
- package/CHANGELOG.md +28 -0
- package/Dockerfile +32 -0
- package/README.md +20 -0
- 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 +1 -1
- package/docs/release-notes/RELEASE_NOTES.md +17 -0
- package/package.json +7 -2
- 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/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
- package/verify-db-mysql.js +0 -92
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
import { createUser } from '../web/lib/auth.js';
|
|
5
|
+
|
|
6
|
+
const { Pool } = pg;
|
|
7
|
+
|
|
8
|
+
// Get database configuration from environment
|
|
9
|
+
const dbUrl = process.env.DEV_WORKFLOW_DB_URL;
|
|
10
|
+
|
|
11
|
+
if (!dbUrl) {
|
|
12
|
+
console.error('Error: DEV_WORKFLOW_DB_URL environment variable is not set');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pool = new Pool({ connectionString: dbUrl });
|
|
17
|
+
|
|
18
|
+
async function createAdminUser() {
|
|
19
|
+
const username = process.env.ADMIN_USERNAME || 'admin';
|
|
20
|
+
const password = process.env.ADMIN_PASSWORD || 'admin123';
|
|
21
|
+
const email = process.env.ADMIN_EMAIL || 'admin@example.com';
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Check if admin user already exists
|
|
25
|
+
const existing = await pool.query('SELECT id FROM users WHERE username = $1', [username]);
|
|
26
|
+
|
|
27
|
+
if (existing.rows.length > 0) {
|
|
28
|
+
console.log(`✓ Admin user '${username}' already exists`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create admin user
|
|
33
|
+
const user = await createUser(pool, username, password, email);
|
|
34
|
+
console.log(`✓ Created admin user: ${user.username}`);
|
|
35
|
+
console.log(` Email: ${user.email}`);
|
|
36
|
+
console.log(` Password: ${password}`);
|
|
37
|
+
console.log('\n⚠️ Please change the password after first login!');
|
|
38
|
+
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error creating admin user:', error.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
} finally {
|
|
43
|
+
await pool.end();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
createAdminUser();
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# GCP Deployment Script using gcloud CLI (PowerShell)
|
|
2
|
+
# This script deploys the dev-workflow MCP server to a GCP Compute Engine instance
|
|
3
|
+
|
|
4
|
+
$ErrorActionPreference = "Stop"
|
|
5
|
+
|
|
6
|
+
# Configuration - Update these values
|
|
7
|
+
$PROJECT_ID = if ($env:GCP_PROJECT_ID) { $env:GCP_PROJECT_ID } else { "edmee-project" }
|
|
8
|
+
$INSTANCE_NAME = if ($env:GCP_INSTANCE_NAME) { $env:GCP_INSTANCE_NAME } else { "docker-almi" }
|
|
9
|
+
$ZONE = if ($env:GCP_ZONE) { $env:GCP_ZONE } else { "asia-southeast2-a" }
|
|
10
|
+
$INSTANCE_USER = if ($env:GCP_INSTANCE_USER) { $env:GCP_INSTANCE_USER } else { "dhika" }
|
|
11
|
+
|
|
12
|
+
Write-Host "🚀 Deploying dev-workflow MCP server to GCP..." -ForegroundColor Cyan
|
|
13
|
+
Write-Host "Project: $PROJECT_ID"
|
|
14
|
+
Write-Host "Instance: $INSTANCE_NAME"
|
|
15
|
+
Write-Host "Zone: $ZONE"
|
|
16
|
+
Write-Host ""
|
|
17
|
+
|
|
18
|
+
# Check if gcloud is installed
|
|
19
|
+
if (!(Get-Command gcloud -ErrorAction SilentlyContinue)) {
|
|
20
|
+
Write-Host "❌ gcloud CLI is not installed" -ForegroundColor Red
|
|
21
|
+
Write-Host "Please install it from: https://cloud.google.com/sdk/docs/install"
|
|
22
|
+
exit 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Set the project
|
|
26
|
+
Write-Host "📋 Setting GCP project..." -ForegroundColor Yellow
|
|
27
|
+
gcloud config set project $PROJECT_ID
|
|
28
|
+
|
|
29
|
+
# Get instance external IP
|
|
30
|
+
Write-Host "🔍 Getting instance external IP..." -ForegroundColor Yellow
|
|
31
|
+
$EXTERNAL_IP = gcloud compute instances describe $INSTANCE_NAME `
|
|
32
|
+
--zone=$ZONE `
|
|
33
|
+
--format="get(networkInterfaces[0].accessConfigs[0].natIP)"
|
|
34
|
+
|
|
35
|
+
if ([string]::IsNullOrEmpty($EXTERNAL_IP)) {
|
|
36
|
+
Write-Host "❌ Could not get instance external IP" -ForegroundColor Red
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Write-Host "✅ Instance IP: $EXTERNAL_IP" -ForegroundColor Green
|
|
41
|
+
Write-Host ""
|
|
42
|
+
|
|
43
|
+
# Test SSH connection
|
|
44
|
+
Write-Host "🔐 Testing SSH connection..." -ForegroundColor Yellow
|
|
45
|
+
try {
|
|
46
|
+
$testResult = gcloud compute ssh "$INSTANCE_USER@$INSTANCE_NAME" `
|
|
47
|
+
--zone=$ZONE `
|
|
48
|
+
--command="echo 'Connection successful'" 2>&1
|
|
49
|
+
Write-Host "✅ SSH connection successful" -ForegroundColor Green
|
|
50
|
+
} catch {
|
|
51
|
+
Write-Host "❌ Cannot connect to instance via SSH" -ForegroundColor Red
|
|
52
|
+
Write-Host "Please ensure:"
|
|
53
|
+
Write-Host " 1. SSH keys are configured in GCP"
|
|
54
|
+
Write-Host " 2. Firewall allows SSH (port 22)"
|
|
55
|
+
Write-Host " 3. Instance is running"
|
|
56
|
+
exit 1
|
|
57
|
+
}
|
|
58
|
+
Write-Host ""
|
|
59
|
+
|
|
60
|
+
# Copy files to instance
|
|
61
|
+
Write-Host "📦 Copying project files to instance..." -ForegroundColor Yellow
|
|
62
|
+
gcloud compute scp --recurse `
|
|
63
|
+
--zone=$ZONE `
|
|
64
|
+
. "$INSTANCE_USER@${INSTANCE_NAME}:~/dev-workflow-mcp-server/" `
|
|
65
|
+
--exclude=".git" `
|
|
66
|
+
--exclude="node_modules" `
|
|
67
|
+
--exclude=".state" `
|
|
68
|
+
--exclude="dist"
|
|
69
|
+
|
|
70
|
+
Write-Host "✅ Files copied successfully" -ForegroundColor Green
|
|
71
|
+
Write-Host ""
|
|
72
|
+
|
|
73
|
+
# Create deployment script for remote execution
|
|
74
|
+
$deployScript = @'
|
|
75
|
+
set -e
|
|
76
|
+
|
|
77
|
+
cd ~/dev-workflow-mcp-server
|
|
78
|
+
|
|
79
|
+
echo "📥 Installing Docker if not present..."
|
|
80
|
+
if ! command -v docker &> /dev/null; then
|
|
81
|
+
curl -fsSL https://get.docker.com -o get-docker.sh
|
|
82
|
+
sudo sh get-docker.sh
|
|
83
|
+
sudo usermod -aG docker $USER
|
|
84
|
+
rm get-docker.sh
|
|
85
|
+
echo "✅ Docker installed"
|
|
86
|
+
else
|
|
87
|
+
echo "✅ Docker already installed"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
echo "📥 Installing Docker Compose if not present..."
|
|
91
|
+
if ! command -v docker-compose &> /dev/null; then
|
|
92
|
+
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
93
|
+
sudo chmod +x /usr/local/bin/docker-compose
|
|
94
|
+
echo "✅ Docker Compose installed"
|
|
95
|
+
else
|
|
96
|
+
echo "✅ Docker Compose already installed"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
echo "⚙️ Configuring environment..."
|
|
100
|
+
if [ ! -f .env ]; then
|
|
101
|
+
cp .env.production .env
|
|
102
|
+
echo "⚠️ Created .env file from template"
|
|
103
|
+
echo "⚠️ Please update the passwords in .env before production use!"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
echo "🛑 Stopping existing containers..."
|
|
107
|
+
docker-compose down 2>/dev/null || true
|
|
108
|
+
|
|
109
|
+
echo "🏗️ Building Docker images..."
|
|
110
|
+
docker-compose build --no-cache
|
|
111
|
+
|
|
112
|
+
echo "🚀 Starting containers..."
|
|
113
|
+
docker-compose up -d
|
|
114
|
+
|
|
115
|
+
echo "⏳ Waiting for services to be healthy..."
|
|
116
|
+
sleep 15
|
|
117
|
+
|
|
118
|
+
echo "📊 Checking container status..."
|
|
119
|
+
docker-compose ps
|
|
120
|
+
|
|
121
|
+
echo ""
|
|
122
|
+
echo "✅ Deployment complete!"
|
|
123
|
+
echo ""
|
|
124
|
+
echo "Container logs:"
|
|
125
|
+
docker-compose logs --tail=20
|
|
126
|
+
'@
|
|
127
|
+
|
|
128
|
+
# Run deployment commands on the instance
|
|
129
|
+
Write-Host "🚀 Running deployment on instance..." -ForegroundColor Yellow
|
|
130
|
+
$deployScript | gcloud compute ssh "$INSTANCE_USER@$INSTANCE_NAME" `
|
|
131
|
+
--zone=$ZONE `
|
|
132
|
+
--command="bash -s"
|
|
133
|
+
|
|
134
|
+
Write-Host ""
|
|
135
|
+
Write-Host "🎉 Deployment successful!" -ForegroundColor Green
|
|
136
|
+
Write-Host ""
|
|
137
|
+
Write-Host "📝 Next steps:" -ForegroundColor Cyan
|
|
138
|
+
Write-Host "1. SSH into instance and update .env with secure passwords:"
|
|
139
|
+
Write-Host " gcloud compute ssh $INSTANCE_USER@$INSTANCE_NAME --zone=$ZONE"
|
|
140
|
+
Write-Host " cd ~/dev-workflow-mcp-server"
|
|
141
|
+
Write-Host " nano .env"
|
|
142
|
+
Write-Host ""
|
|
143
|
+
Write-Host "2. Update your local MCP client config:" -ForegroundColor Cyan
|
|
144
|
+
Write-Host @"
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"dev-workflow": {
|
|
148
|
+
"command": "gcloud",
|
|
149
|
+
"args": [
|
|
150
|
+
"compute", "ssh", "$INSTANCE_USER@$INSTANCE_NAME",
|
|
151
|
+
"--zone=$ZONE",
|
|
152
|
+
"--command=docker exec -i dev-workflow-mcp node /app/index.js"
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
"@
|
|
158
|
+
Write-Host ""
|
|
159
|
+
Write-Host "3. Test the connection:" -ForegroundColor Cyan
|
|
160
|
+
Write-Host " gcloud compute ssh $INSTANCE_USER@$INSTANCE_NAME --zone=$ZONE --command='docker exec -i dev-workflow-mcp node /app/index.js call get_workflow_status'"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GCP Deployment Script using gcloud CLI
|
|
3
|
+
# This script deploys the dev-workflow MCP server to a GCP Compute Engine instance
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# Configuration - Update these values
|
|
8
|
+
PROJECT_ID="${GCP_PROJECT_ID:-edmee-project}"
|
|
9
|
+
INSTANCE_NAME="${GCP_INSTANCE_NAME:-docker-almi}"
|
|
10
|
+
ZONE="${GCP_ZONE:-asia-southeast2-a}"
|
|
11
|
+
INSTANCE_USER="${GCP_INSTANCE_USER:-dhika}"
|
|
12
|
+
|
|
13
|
+
echo "🚀 Deploying dev-workflow MCP server to GCP..."
|
|
14
|
+
echo "Project: $PROJECT_ID"
|
|
15
|
+
echo "Instance: $INSTANCE_NAME"
|
|
16
|
+
echo "Zone: $ZONE"
|
|
17
|
+
echo ""
|
|
18
|
+
|
|
19
|
+
# Check if gcloud is installed
|
|
20
|
+
if ! command -v gcloud &> /dev/null; then
|
|
21
|
+
echo "❌ gcloud CLI is not installed"
|
|
22
|
+
echo "Please install it from: https://cloud.google.com/sdk/docs/install"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Set the project
|
|
27
|
+
echo "📋 Setting GCP project..."
|
|
28
|
+
gcloud config set project "$PROJECT_ID"
|
|
29
|
+
|
|
30
|
+
# Get instance external IP
|
|
31
|
+
echo "🔍 Getting instance external IP..."
|
|
32
|
+
EXTERNAL_IP=$(gcloud compute instances describe "$INSTANCE_NAME" \
|
|
33
|
+
--zone="$ZONE" \
|
|
34
|
+
--format="get(networkInterfaces[0].accessConfigs[0].natIP)")
|
|
35
|
+
|
|
36
|
+
if [ -z "$EXTERNAL_IP" ]; then
|
|
37
|
+
echo "❌ Could not get instance external IP"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "✅ Instance IP: $EXTERNAL_IP"
|
|
42
|
+
echo ""
|
|
43
|
+
|
|
44
|
+
# Test SSH connection
|
|
45
|
+
echo "🔐 Testing SSH connection..."
|
|
46
|
+
if ! gcloud compute ssh "$INSTANCE_USER@$INSTANCE_NAME" \
|
|
47
|
+
--zone="$ZONE" \
|
|
48
|
+
--command="echo 'Connection successful'" &> /dev/null; then
|
|
49
|
+
echo "❌ Cannot connect to instance via SSH"
|
|
50
|
+
echo "Please ensure:"
|
|
51
|
+
echo " 1. SSH keys are configured in GCP"
|
|
52
|
+
echo " 2. Firewall allows SSH (port 22)"
|
|
53
|
+
echo " 3. Instance is running"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
echo "✅ SSH connection successful"
|
|
57
|
+
echo ""
|
|
58
|
+
|
|
59
|
+
# Copy files to instance
|
|
60
|
+
echo "📦 Copying project files to instance..."
|
|
61
|
+
gcloud compute scp --recurse \
|
|
62
|
+
--zone="$ZONE" \
|
|
63
|
+
. "$INSTANCE_USER@$INSTANCE_NAME:~/dev-workflow-mcp-server/" \
|
|
64
|
+
--exclude=".git" \
|
|
65
|
+
--exclude="node_modules" \
|
|
66
|
+
--exclude=".state" \
|
|
67
|
+
--exclude="dist"
|
|
68
|
+
|
|
69
|
+
echo "✅ Files copied successfully"
|
|
70
|
+
echo ""
|
|
71
|
+
|
|
72
|
+
# Run deployment commands on the instance
|
|
73
|
+
echo "🚀 Running deployment on instance..."
|
|
74
|
+
gcloud compute ssh "$INSTANCE_USER@$INSTANCE_NAME" \
|
|
75
|
+
--zone="$ZONE" \
|
|
76
|
+
--command="bash -s" << 'ENDSSH'
|
|
77
|
+
set -e
|
|
78
|
+
|
|
79
|
+
cd ~/dev-workflow-mcp-server
|
|
80
|
+
|
|
81
|
+
echo "📥 Installing Docker if not present..."
|
|
82
|
+
if ! command -v docker &> /dev/null; then
|
|
83
|
+
curl -fsSL https://get.docker.com -o get-docker.sh
|
|
84
|
+
sudo sh get-docker.sh
|
|
85
|
+
sudo usermod -aG docker $USER
|
|
86
|
+
rm get-docker.sh
|
|
87
|
+
echo "✅ Docker installed"
|
|
88
|
+
else
|
|
89
|
+
echo "✅ Docker already installed"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
echo "📥 Installing Docker Compose if not present..."
|
|
93
|
+
if ! command -v docker-compose &> /dev/null; then
|
|
94
|
+
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
95
|
+
sudo chmod +x /usr/local/bin/docker-compose
|
|
96
|
+
echo "✅ Docker Compose installed"
|
|
97
|
+
else
|
|
98
|
+
echo "✅ Docker Compose already installed"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
echo "⚙️ Configuring environment..."
|
|
102
|
+
if [ ! -f .env ]; then
|
|
103
|
+
cp .env.production .env
|
|
104
|
+
echo "⚠️ Created .env file from template"
|
|
105
|
+
echo "⚠️ Please update the passwords in .env before production use!"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
echo "🛑 Stopping existing containers..."
|
|
109
|
+
docker-compose down 2>/dev/null || true
|
|
110
|
+
|
|
111
|
+
echo "🏗️ Building Docker images..."
|
|
112
|
+
docker-compose build --no-cache
|
|
113
|
+
|
|
114
|
+
echo "🚀 Starting containers..."
|
|
115
|
+
docker-compose up -d
|
|
116
|
+
|
|
117
|
+
echo "⏳ Waiting for services to be healthy..."
|
|
118
|
+
sleep 15
|
|
119
|
+
|
|
120
|
+
echo "📊 Checking container status..."
|
|
121
|
+
docker-compose ps
|
|
122
|
+
|
|
123
|
+
echo ""
|
|
124
|
+
echo "✅ Deployment complete!"
|
|
125
|
+
echo ""
|
|
126
|
+
echo "Container logs:"
|
|
127
|
+
docker-compose logs --tail=20
|
|
128
|
+
ENDSSH
|
|
129
|
+
|
|
130
|
+
echo ""
|
|
131
|
+
echo "🎉 Deployment successful!"
|
|
132
|
+
echo ""
|
|
133
|
+
echo "📝 Next steps:"
|
|
134
|
+
echo "1. SSH into instance and update .env with secure passwords:"
|
|
135
|
+
echo " gcloud compute ssh $INSTANCE_USER@$INSTANCE_NAME --zone=$ZONE"
|
|
136
|
+
echo " cd ~/dev-workflow-mcp-server"
|
|
137
|
+
echo " nano .env"
|
|
138
|
+
echo ""
|
|
139
|
+
echo "2. Update your local MCP client config:"
|
|
140
|
+
echo " {"
|
|
141
|
+
echo " \"mcpServers\": {"
|
|
142
|
+
echo " \"dev-workflow\": {"
|
|
143
|
+
echo " \"command\": \"gcloud\","
|
|
144
|
+
echo " \"args\": ["
|
|
145
|
+
echo " \"compute\", \"ssh\", \"$INSTANCE_USER@$INSTANCE_NAME\","
|
|
146
|
+
echo " \"--zone=$ZONE\","
|
|
147
|
+
echo " \"--command=docker exec -i dev-workflow-mcp node /app/index.js\""
|
|
148
|
+
echo " ]"
|
|
149
|
+
echo " }"
|
|
150
|
+
echo " }"
|
|
151
|
+
echo " }"
|
|
152
|
+
echo ""
|
|
153
|
+
echo "3. Test the connection:"
|
|
154
|
+
echo " gcloud compute ssh $INSTANCE_USER@$INSTANCE_NAME --zone=$ZONE --command='docker exec -i dev-workflow-mcp node /app/index.js call get_workflow_status'"
|
|
@@ -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 />
|