Skip to content

Commit cde816c

Browse files
Implement Project Dashboard and Detail UI with deployment orchestration
- Add ProjectCard component and ProjectDetailPage - Implement deployment triggering and real-time status polling - Add configuration management for Web Services and Static Sites - Implement secret (Environment Variables) management with masking - Add comprehensive styling for the project lifecycle dashboard Co-authored-by: Gemini CLI
1 parent 89d56ed commit cde816c

7 files changed

Lines changed: 655 additions & 32 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ coverage.out
2828

2929
# Docker
3030
.docker/
31+
data/
32+
cookies.txt
33+
backend/worker

backend/internal/httpapi/httpapi.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,12 +329,13 @@ func (h *Handler) handleProjectByID(w http.ResponseWriter, r *http.Request) {
329329
}
330330

331331
path := strings.TrimPrefix(r.URL.Path, "/projects/")
332-
parts := strings.Split(path, "/")
333-
if len(parts) == 0 || parts[0] == "" {
332+
path = strings.TrimSuffix(path, "/")
333+
if path == "" {
334334
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
335335
return
336336
}
337337

338+
parts := strings.Split(path, "/")
338339
projectID := parts[0]
339340

340341
if len(parts) == 2 {
@@ -356,10 +357,13 @@ func (h *Handler) handleProjectByID(w http.ResponseWriter, r *http.Request) {
356357
}
357358
writeMethodNotAllowed(w, http.MethodGet)
358359
return
360+
default:
361+
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
362+
return
359363
}
360364
}
361365

362-
if len(parts) > 1 {
366+
if len(parts) > 2 {
363367
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
364368
return
365369
}
@@ -563,12 +567,13 @@ func (h *Handler) handleDeploymentByID(w http.ResponseWriter, r *http.Request) {
563567
}
564568

565569
path := strings.TrimPrefix(r.URL.Path, "/deployments/")
566-
parts := strings.Split(path, "/")
567-
if len(parts) == 0 || parts[0] == "" {
570+
path = strings.TrimSuffix(path, "/")
571+
if path == "" {
568572
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
569573
return
570574
}
571575

576+
parts := strings.Split(path, "/")
572577
deploymentID := parts[0]
573578

574579
if len(parts) == 2 && parts[1] == "events" {

backend/internal/project/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ func (s *Service) GetConfig(ctx context.Context, userID, projectID string) (Proj
251251
)
252252
if err != nil {
253253
if errors.Is(err, pgx.ErrNoRows) {
254-
return ProjectConfig{}, ErrConfigNotFound
254+
return ProjectConfig{ProjectID: projectID}, nil
255255
}
256256
return ProjectConfig{}, fmt.Errorf("get config: %w", err)
257257
}
@@ -302,7 +302,7 @@ func (s *Service) GetConfigInternal(ctx context.Context, projectID string) (Proj
302302
)
303303
if err != nil {
304304
if errors.Is(err, pgx.ErrNoRows) {
305-
return ProjectConfig{}, ErrConfigNotFound
305+
return ProjectConfig{ProjectID: projectID}, nil
306306
}
307307
return ProjectConfig{}, fmt.Errorf("get config internal: %w", err)
308308
}

frontend/src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { LoginPage } from './pages/LoginPage';
55
import { SignupPage } from './pages/SignupPage';
66
import { DashboardPage } from './pages/DashboardPage';
77

8+
import { ProjectDetailPage } from './pages/ProjectDetailPage';
9+
810
export default function App() {
911
return (
1012
<BrowserRouter>
@@ -21,6 +23,14 @@ export default function App() {
2123
</ProtectedRoute>
2224
}
2325
/>
26+
<Route
27+
path="/projects/:id"
28+
element={
29+
<ProtectedRoute>
30+
<ProjectDetailPage />
31+
</ProtectedRoute>
32+
}
33+
/>
2434
</Routes>
2535
</AuthProvider>
2636
</BrowserRouter>

frontend/src/pages/DashboardPage.tsx

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,62 @@
11
import { useState, useEffect, FormEvent } from 'react';
2-
import { api, Project } from '../api/client';
2+
import { Link } from 'react-router-dom';
3+
import { api, Project, Deployment, APIError } from '../api/client';
34
import { Navigation } from '../components/Navigation';
4-
import { APIError } from '../api/client';
5+
6+
function ProjectCard({ project }: { project: Project }) {
7+
const [latestDeployment, setLatestDeployment] = useState<Deployment | null>(null);
8+
const [loading, setLoading] = useState(true);
9+
10+
useEffect(() => {
11+
loadLatestDeployment();
12+
}, [project.id]);
13+
14+
const loadLatestDeployment = async () => {
15+
try {
16+
const deployments = await api.projects.listDeployments(project.id);
17+
if (deployments.length > 0) {
18+
setLatestDeployment(deployments[0]);
19+
}
20+
} catch (err) {
21+
console.error('Failed to load deployments', err);
22+
} finally {
23+
setLoading(false);
24+
}
25+
};
26+
27+
return (
28+
<Link to={`/projects/${project.id}`} className="project-card-link">
29+
<div className="project-card clickable">
30+
<div className="project-card-header">
31+
<h3>{project.name}</h3>
32+
<span className={`workload-badge ${project.workload_type}`}>
33+
{project.workload_type === 'web_service' ? 'Web Service' : 'Static Site'}
34+
</span>
35+
</div>
36+
37+
<div className="project-details">
38+
<p><strong>Repository:</strong> {project.git_repo_url}</p>
39+
<p><strong>Branch:</strong> {project.branch}</p>
40+
</div>
41+
42+
<div className="deployment-summary">
43+
{loading ? (
44+
<p className="status-text">Loading...</p>
45+
) : latestDeployment ? (
46+
<div className="status-line">
47+
<span className={`status-badge ${latestDeployment.status}`}>
48+
{latestDeployment.status}
49+
</span>
50+
{latestDeployment.status === 'running' && <span className="live-indicator">● Live</span>}
51+
</div>
52+
) : (
53+
<p className="status-text">No deployments</p>
54+
)}
55+
</div>
56+
</div>
57+
</Link>
58+
);
59+
}
560

661
export function DashboardPage() {
762
const [projects, setProjects] = useState<Project[]>([]);
@@ -50,7 +105,7 @@ export function DashboardPage() {
50105
formData.branch,
51106
formData.workloadType
52107
);
53-
setProjects([...projects, newProject]);
108+
setProjects([newProject, ...projects]);
54109
setShowCreateForm(false);
55110
setFormData({ name: '', gitRepoUrl: '', branch: 'main', workloadType: 'web_service' });
56111
} catch (err) {
@@ -174,27 +229,7 @@ export function DashboardPage() {
174229
) : (
175230
<div className="projects-list">
176231
{projects.map((project) => (
177-
<div key={project.id} className="project-card">
178-
<div className="project-card-header">
179-
<h3>{project.name}</h3>
180-
<span className={`workload-badge ${project.workload_type}`}>
181-
{project.workload_type === 'web_service'
182-
? 'Web Service'
183-
: 'Static Site'}
184-
</span>
185-
</div>
186-
<div className="project-details">
187-
<p>
188-
<strong>Repository:</strong> {project.git_repo_url}
189-
</p>
190-
<p>
191-
<strong>Branch:</strong> {project.branch}
192-
</p>
193-
<p className="project-date">
194-
Created {new Date(project.created_at).toLocaleDateString()}
195-
</p>
196-
</div>
197-
</div>
232+
<ProjectCard key={project.id} project={project} />
198233
))}
199234
</div>
200235
)}

0 commit comments

Comments
 (0)