Skip to content

Commit a3a037f

Browse files
committed
Fix GUI handling for missing runtime.yml and add 1-click project initialization
1 parent 9079e65 commit a3a037f

3 files changed

Lines changed: 174 additions & 32 deletions

File tree

‎internal/gui/dashboard/index.html‎

Lines changed: 115 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,22 @@
8787
}
8888
.tab-content { display: none; flex-direction: column; gap: 1.5rem; }
8989
.tab-content.active { display: flex; }
90+
.alert-banner {
91+
background: rgba(245, 158, 11, 0.1);
92+
border: 1px solid rgba(245, 158, 11, 0.3);
93+
color: var(--warning);
94+
padding: 1rem 1.25rem;
95+
border-radius: 8px;
96+
display: flex;
97+
justify-content: space-between;
98+
align-items: center;
99+
font-size: 0.95rem;
100+
}
101+
.alert-banner.danger {
102+
background: rgba(244, 63, 94, 0.1);
103+
border-color: rgba(244, 63, 94, 0.3);
104+
color: var(--danger);
105+
}
90106
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 1.5rem; }
91107
.card {
92108
background: var(--card-bg);
@@ -132,14 +148,15 @@
132148
align-items: center;
133149
justify-content: center;
134150
gap: 0.5rem;
135-
transition: background 0.2s ease;
151+
transition: background 0.2s ease, opacity 0.2s ease;
136152
}
153+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
137154
.btn-primary { background: var(--primary); color: #0f172a; }
138-
.btn-primary:hover { background: var(--primary-hover); }
155+
.btn-primary:hover:not(:disabled) { background: var(--primary-hover); }
139156
.btn-danger { background: rgba(244, 63, 94, 0.15); color: var(--danger); border: 1px solid rgba(244, 63, 94, 0.3); }
140-
.btn-danger:hover { background: rgba(244, 63, 94, 0.3); }
157+
.btn-danger:hover:not(:disabled) { background: rgba(244, 63, 94, 0.3); }
141158
.btn-secondary { background: var(--card-border); color: var(--text); }
142-
.btn-secondary:hover { background: #334155; }
159+
.btn-secondary:hover:not(:disabled) { background: #334155; }
143160
.log-box {
144161
background: #090d16;
145162
border: 1px solid var(--card-border);
@@ -163,8 +180,6 @@
163180
border-radius: 8px;
164181
}
165182
.health-check.ok { border-left: 4px solid var(--success); }
166-
.health-check.warn { border-left: 4px solid var(--warning); }
167-
.health-check.error { border-left: 4px solid var(--danger); }
168183
</style>
169184
</head>
170185
<body>
@@ -174,14 +189,26 @@
174189
<span class="logo-badge">DESKTOP GUI</span>
175190
</div>
176191
<nav>
177-
<button class="nav-btn active" onclick="switchTab('projects')">Projects</button>
178-
<button class="nav-btn" onclick="switchTab('packs')">Runtime Packs</button>
179-
<button class="nav-btn" onclick="switchTab('logs')">Live Logs</button>
180-
<button class="nav-btn" onclick="switchTab('doctor')">Doctor</button>
192+
<button class="nav-btn active" onclick="switchTab('projects', event)">Projects</button>
193+
<button class="nav-btn" onclick="switchTab('packs', event)">Runtime Packs</button>
194+
<button class="nav-btn" onclick="switchTab('logs', event)">Live Logs</button>
195+
<button class="nav-btn" onclick="switchTab('doctor', event)">Doctor</button>
181196
</nav>
182197
</header>
183198

184199
<main>
200+
<!-- Notification Banner -->
201+
<div id="notice-banner" class="alert-banner" style="display:none;">
202+
<span id="notice-message">No runtime.yml found in current directory.</span>
203+
<button class="btn btn-primary" onclick="initProject()" style="flex:none;">Create runtime.yml</button>
204+
</div>
205+
206+
<!-- Error Banner -->
207+
<div id="error-banner" class="alert-banner danger" style="display:none;">
208+
<span id="error-message"></span>
209+
<button class="btn btn-secondary" onclick="hideError()" style="flex:none;">Dismiss</button>
210+
</div>
211+
185212
<!-- Projects Tab -->
186213
<section id="tab-projects" class="tab-content active">
187214
<h2>Active Projects & Services</h2>
@@ -197,8 +224,8 @@ <h2>Active Projects & Services</h2>
197224
<div class="meta-item"><span>Database:</span><span class="meta-value" id="current-db">-</span></div>
198225
</div>
199226
<div class="btn-group">
200-
<button class="btn btn-primary" onclick="startServices()">Start Services</button>
201-
<button class="btn btn-danger" onclick="stopServices()">Stop Services</button>
227+
<button class="btn btn-primary" id="btn-start" onclick="startServices()">Start Services</button>
228+
<button class="btn btn-danger" id="btn-stop" onclick="stopServices()">Stop Services</button>
202229
</div>
203230
</div>
204231
</div>
@@ -248,19 +275,43 @@ <h2>Environment Diagnostics</h2>
248275
</main>
249276

250277
<script>
251-
function switchTab(tabId) {
278+
function switchTab(tabId, evt) {
252279
document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active'));
253280
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
254-
event.target.classList.add('active');
281+
if (evt && evt.target) evt.target.classList.add('active');
255282
document.getElementById('tab-' + tabId).classList.add('active');
256283
if (tabId === 'logs') fetchLogs();
257284
if (tabId === 'doctor') fetchDoctor();
258285
}
259286

287+
function showError(msg) {
288+
const banner = document.getElementById('error-banner');
289+
document.getElementById('error-message').innerText = msg;
290+
banner.style.display = 'flex';
291+
}
292+
293+
function hideError() {
294+
document.getElementById('error-banner').style.display = 'none';
295+
}
296+
260297
async function fetchStatus() {
261298
try {
262299
const res = await fetch('/api/status');
263300
const data = await res.json();
301+
302+
if (!data.manifest_found) {
303+
document.getElementById('notice-banner').style.display = 'flex';
304+
document.getElementById('notice-message').innerText = data.error || 'No runtime.yml found in current directory.';
305+
document.getElementById('current-project-name').innerText = 'No Manifest';
306+
document.getElementById('btn-start').disabled = true;
307+
document.getElementById('btn-stop').disabled = true;
308+
return;
309+
}
310+
311+
document.getElementById('notice-banner').style.display = 'none';
312+
document.getElementById('btn-start').disabled = false;
313+
document.getElementById('btn-stop').disabled = false;
314+
264315
if (data.project) {
265316
document.getElementById('current-project-name').innerText = data.project.name || 'Local Project';
266317
document.getElementById('current-php').innerText = 'PHP ' + (data.project.php || '8.3');
@@ -277,14 +328,60 @@ <h2>Environment Diagnostics</h2>
277328
}
278329
}
279330

331+
async function initProject() {
332+
hideError();
333+
try {
334+
const res = await fetch('/api/init', { method: 'POST' });
335+
const data = await res.json();
336+
if (data.success) {
337+
document.getElementById('notice-banner').style.display = 'none';
338+
fetchStatus();
339+
} else {
340+
showError(data.error || 'Failed to create runtime.yml');
341+
}
342+
} catch (err) {
343+
showError('Network error while initializing project.');
344+
}
345+
}
346+
280347
async function startServices() {
281-
await fetch('/api/up', { method: 'POST' });
282-
fetchStatus();
348+
hideError();
349+
const startBtn = document.getElementById('btn-start');
350+
startBtn.disabled = true;
351+
startBtn.innerText = 'Starting...';
352+
try {
353+
const res = await fetch('/api/up', { method: 'POST' });
354+
const data = await res.json();
355+
if (!data.success) {
356+
showError(data.error || 'Failed to start services.');
357+
}
358+
} catch (err) {
359+
showError('Network error while starting services.');
360+
} finally {
361+
startBtn.disabled = false;
362+
startBtn.innerText = 'Start Services';
363+
fetchStatus();
364+
}
283365
}
284366

285367
async function stopServices() {
286-
await fetch('/api/down', { method: 'POST' });
287-
fetchStatus();
368+
hideError();
369+
const stopBtn = document.getElementById('btn-stop');
370+
stopBtn.disabled = true;
371+
stopBtn.innerText = 'Stopping...';
372+
try {
373+
const res = await fetch('/api/down', { method: 'POST' });
374+
const data = await res.json();
375+
if (!data.success) {
376+
showError(data.error || 'Failed to stop services.');
377+
}
378+
} catch (err) {
379+
showError('Network error while stopping services.');
380+
} finally {
381+
stopBtn.disabled = false;
382+
stopBtn.innerText = 'Stop Services';
383+
fetchStatus();
384+
}
288385
}
289386

290387
async function fetchLogs() {

‎internal/gui/server.go‎

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (s *Server) Start() (string, error) {
4242
})
4343

4444
mux.HandleFunc("/api/status", handleStatus)
45+
mux.HandleFunc("/api/init", handleInit)
4546
mux.HandleFunc("/api/up", handleUp)
4647
mux.HandleFunc("/api/down", handleDown)
4748
mux.HandleFunc("/api/logs", handleLogs)
@@ -61,13 +62,17 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
6162
w.Header().Set("Content-Type", "application/json")
6263
cwd, err := os.Getwd()
6364
if err != nil {
64-
json.NewEncoder(w).Encode(map[string]interface{}{"error": err.Error()})
65+
json.NewEncoder(w).Encode(map[string]interface{}{"error": err.Error(), "manifest_found": false})
6566
return
6667
}
6768

6869
m, err := manifest.Find(cwd)
6970
if err != nil {
70-
json.NewEncoder(w).Encode(map[string]interface{}{"project": nil})
71+
json.NewEncoder(w).Encode(map[string]interface{}{
72+
"manifest_found": false,
73+
"project_dir": cwd,
74+
"error": "No runtime.yml found in " + cwd,
75+
})
7176
return
7277
}
7378

@@ -97,6 +102,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
97102
}
98103

99104
json.NewEncoder(w).Encode(map[string]interface{}{
105+
"manifest_found": true,
100106
"project": map[string]interface{}{
101107
"name": name,
102108
"php": m.Runtime.PHP,
@@ -107,53 +113,91 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
107113
})
108114
}
109115

116+
func handleInit(w http.ResponseWriter, r *http.Request) {
117+
w.Header().Set("Content-Type", "application/json")
118+
if r.Method != http.MethodPost {
119+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": "Method not allowed"})
120+
return
121+
}
122+
cwd, err := os.Getwd()
123+
if err != nil {
124+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": err.Error()})
125+
return
126+
}
127+
manifestPath := filepath.Join(cwd, "runtime.yml")
128+
name := filepath.Base(cwd)
129+
content := strings.Join([]string{
130+
"# runtime.yml - managed by phpvm",
131+
`phpvm: "1"`,
132+
"",
133+
"runtime:",
134+
` php: "8.3"`,
135+
"",
136+
"server:",
137+
" engine: nginx",
138+
" root: public",
139+
"",
140+
"project:",
141+
" name: " + name,
142+
" domain: " + strings.ToLower(name) + ".local",
143+
"",
144+
}, "\n")
145+
146+
if err := os.WriteFile(manifestPath, []byte(content), 0644); err != nil {
147+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": err.Error()})
148+
return
149+
}
150+
151+
json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "message": "Created runtime.yml"})
152+
}
153+
110154
func handleUp(w http.ResponseWriter, r *http.Request) {
155+
w.Header().Set("Content-Type", "application/json")
111156
if r.Method != http.MethodPost {
112-
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
157+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": "Method not allowed"})
113158
return
114159
}
115160
cwd, _ := os.Getwd()
116161
m, err := manifest.Find(cwd)
117162
if err != nil {
118-
http.Error(w, err.Error(), http.StatusBadRequest)
163+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": err.Error()})
119164
return
120165
}
121166
rt, err := resolver.ResolveFromManifest(m)
122167
if err != nil {
123-
http.Error(w, err.Error(), http.StatusBadRequest)
168+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": err.Error()})
124169
return
125170
}
126171
ps, _ := state.Load(m.ProjectRoot)
127172
sup := supervisor.New(m, rt, ps)
128173
statuses, err := sup.Up(true, true)
129174
if err != nil {
130-
http.Error(w, err.Error(), http.StatusInternalServerError)
175+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": err.Error(), "services": statuses})
131176
return
132177
}
133-
w.Header().Set("Content-Type", "application/json")
134-
json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok", "services": statuses})
178+
json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "services": statuses})
135179
}
136180

137181
func handleDown(w http.ResponseWriter, r *http.Request) {
182+
w.Header().Set("Content-Type", "application/json")
138183
if r.Method != http.MethodPost {
139-
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
184+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": "Method not allowed"})
140185
return
141186
}
142187
cwd, _ := os.Getwd()
143188
m, err := manifest.Find(cwd)
144189
if err != nil {
145-
http.Error(w, err.Error(), http.StatusBadRequest)
190+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": err.Error()})
146191
return
147192
}
148193
ps, _ := state.Load(m.ProjectRoot)
149194
sup := supervisor.New(m, nil, ps)
150195
statuses, err := sup.Down()
151196
if err != nil {
152-
http.Error(w, err.Error(), http.StatusInternalServerError)
197+
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "error": err.Error(), "services": statuses})
153198
return
154199
}
155-
w.Header().Set("Content-Type", "application/json")
156-
json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok", "services": statuses})
200+
json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "services": statuses})
157201
}
158202

159203
func handleLogs(w http.ResponseWriter, r *http.Request) {
@@ -164,7 +208,8 @@ func handleLogs(w http.ResponseWriter, r *http.Request) {
164208
cwd, _ := os.Getwd()
165209
m, err := manifest.Find(cwd)
166210
if err != nil {
167-
http.Error(w, "Manifest not found", http.StatusBadRequest)
211+
w.Header().Set("Content-Type", "text/plain")
212+
w.Write([]byte("No runtime.yml manifest found in current directory."))
168213
return
169214
}
170215
logDir := paths.ProjectLogDir(m.ProjectRoot)

‎phpvm.exe~‎

14.3 MB
Binary file not shown.

0 commit comments

Comments
 (0)