updated
This commit is contained in:
@@ -59,6 +59,10 @@ func (s *Server) loadTemplates() {
|
||||
switch state {
|
||||
case stacks.StateRunning:
|
||||
return "green"
|
||||
case stacks.StateStarting:
|
||||
return "orange"
|
||||
case stacks.StateUnhealthy:
|
||||
return "yellow"
|
||||
case stacks.StateStopped, stacks.StateExited:
|
||||
return "red"
|
||||
case stacks.StateRestarting:
|
||||
@@ -71,6 +75,10 @@ func (s *Server) loadTemplates() {
|
||||
switch state {
|
||||
case stacks.StateRunning:
|
||||
return "Fut"
|
||||
case stacks.StateStarting:
|
||||
return "Indulás..."
|
||||
case stacks.StateUnhealthy:
|
||||
return "Nem egészséges"
|
||||
case stacks.StateStopped, stacks.StateExited:
|
||||
return "Leállítva"
|
||||
case stacks.StateRestarting:
|
||||
@@ -87,6 +95,10 @@ func (s *Server) loadTemplates() {
|
||||
switch state {
|
||||
case stacks.StateRunning:
|
||||
return "●"
|
||||
case stacks.StateStarting:
|
||||
return "◐"
|
||||
case stacks.StateUnhealthy:
|
||||
return "◑"
|
||||
case stacks.StateStopped, stacks.StateExited:
|
||||
return "○"
|
||||
case stacks.StateRestarting:
|
||||
@@ -98,6 +110,16 @@ func (s *Server) loadTemplates() {
|
||||
"stateStr": func(state stacks.ContainerState) string {
|
||||
return string(state)
|
||||
},
|
||||
// isOperational returns true for any state where the stack has containers
|
||||
// and is not stopped/exited — used by templates for showing action buttons
|
||||
"isOperational": func(state stacks.ContainerState) bool {
|
||||
switch state {
|
||||
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
"logoURL": func(slug string) string {
|
||||
return s.cfg.AppLogoURL(slug)
|
||||
},
|
||||
@@ -134,7 +156,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
fmt.Fprint(w, cssContent)
|
||||
case strings.HasPrefix(path, "/static/assets/"):
|
||||
// Serve baked-in app assets (logos, screenshots)
|
||||
s.serveAsset(w, r, strings.TrimPrefix(path, "/static/assets/"))
|
||||
case strings.HasPrefix(path, "/apps/"):
|
||||
slug := strings.TrimPrefix(path, "/apps/")
|
||||
@@ -287,6 +308,10 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
running++
|
||||
case stacks.StateStopped, stacks.StateExited:
|
||||
stopped++
|
||||
case stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
|
||||
// Count starting/unhealthy/restarting as "running" for the dashboard stat
|
||||
// (they have containers, they're just not fully healthy yet)
|
||||
running++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,11 +372,9 @@ func (s *Server) deployHandler(w http.ResponseWriter, _ *http.Request, name stri
|
||||
}
|
||||
|
||||
// serveAsset serves baked-in app assets (logos, screenshots) from /usr/share/felhom/assets/
|
||||
// These are copied into the container at build time.
|
||||
const assetsDir = "/usr/share/felhom/assets"
|
||||
|
||||
func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request, filename string) {
|
||||
// Sanitize: prevent directory traversal
|
||||
filename = filepath.Base(filename)
|
||||
path := filepath.Join(assetsDir, filename)
|
||||
|
||||
@@ -364,14 +387,9 @@ func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request, filename str
|
||||
http.ServeFile(w, r, path)
|
||||
}
|
||||
|
||||
// appDetailHandler serves a local app detail page (description, screenshots, FAQ).
|
||||
// TODO: Phase 1.5 — for now, redirect to the stacks page.
|
||||
// Future: render a dedicated app page template with baked-in content.
|
||||
func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug string) {
|
||||
// Find the stack by slug
|
||||
for _, stack := range s.stackMgr.GetStacks() {
|
||||
if stack.Meta.Slug == slug {
|
||||
// For now, redirect to deploy page (if not deployed) or stacks page
|
||||
if !stack.Deployed {
|
||||
http.Redirect(w, r, "/stacks/"+stack.Name+"/deploy", http.StatusFound)
|
||||
} else {
|
||||
@@ -402,4 +420,4 @@ func (s *Server) render(w http.ResponseWriter, name string, data interface{}) {
|
||||
s.logger.Printf("[ERROR] Template error (%s): %v", name, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ const dashboardTmpl = `
|
||||
{{else if not .Deployed}}
|
||||
<a href="/stacks/{{.Name}}/deploy" class="btn btn-sm btn-primary">🚀 Telepítés</a>
|
||||
{{else}}
|
||||
{{if eq (stateStr .State) "running"}}
|
||||
{{if isOperational .State}}
|
||||
<button class="btn btn-sm btn-warning" onclick="stackAction('{{.Name}}', 'restart')">↻</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="stackAction('{{.Name}}', 'stop')">■</button>
|
||||
{{else}}
|
||||
@@ -192,7 +192,7 @@ const stacksTmpl = `
|
||||
<a href="/stacks/{{.Name}}/deploy" class="btn btn-primary">🚀 Telepítés</a>
|
||||
<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">ℹ️ Részletek</a>
|
||||
{{else}}
|
||||
{{if eq (stateStr .State) "running"}}
|
||||
{{if isOperational .State}}
|
||||
<button class="btn btn-success" onclick="stackAction('{{.Name}}', 'update')">⬆ Frissítés</button>
|
||||
<button class="btn btn-warning" onclick="stackAction('{{.Name}}', 'restart')">↻ Újraindítás</button>
|
||||
<button class="btn btn-danger" onclick="stackAction('{{.Name}}', 'stop')">■ Leállítás</button>
|
||||
@@ -343,7 +343,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side validation: check all required fields are filled
|
||||
// Client-side validation: check all required fields
|
||||
const requiredFields = e.target.querySelectorAll('input[required], select[required]');
|
||||
for (const rf of requiredFields) {
|
||||
if (!rf.disabled && rf.value.trim() === '') {
|
||||
@@ -394,64 +394,6 @@ document.getElementById('deploy-form').addEventListener('submit', async function
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "layout_end" .}}
|
||||
{{end}}
|
||||
` + "\n"
|
||||
|
||||
<script>
|
||||
function generatePassword(fieldId) {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let pass = '';
|
||||
const arr = new Uint8Array(16);
|
||||
crypto.getRandomValues(arr);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
pass += chars[arr[i] % chars.length];
|
||||
}
|
||||
document.getElementById(fieldId).value = pass;
|
||||
}
|
||||
|
||||
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('[type=submit]');
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Telepítés folyamatban...';
|
||||
|
||||
const values = {};
|
||||
const inputs = e.target.querySelectorAll('input, select');
|
||||
inputs.forEach(function(el) {
|
||||
if (el.name && !el.disabled) {
|
||||
if (el.type === 'checkbox') {
|
||||
values[el.name] = el.checked ? 'true' : 'false';
|
||||
} else {
|
||||
values[el.name] = el.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/{{.Stack.Name}}/deploy', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({values: values})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
alert('Hiba: ' + data.error);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
alert('Sikeres telepítés! ✓');
|
||||
window.location.href = '/stacks';
|
||||
} catch (err) {
|
||||
alert('Hálózati hiba: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "layout_end" .}}
|
||||
{{end}}
|
||||
`
|
||||
@@ -512,6 +454,7 @@ const cssContent = `
|
||||
--card-bg:#fff; --text:#1a202c; --text-muted:#718096; --border:#e2e8f0;
|
||||
--green:#38a169; --green-light:#c6f6d5; --red:#e53e3e; --red-light:#fed7d7;
|
||||
--yellow:#d69e2e; --yellow-light:#fefcbf; --blue:#3182ce; --blue-light:#bee3f8;
|
||||
--orange:#dd6b20; --orange-light:#feebc8;
|
||||
--gray:#a0aec0; --gray-light:#edf2f7; --radius:8px; --shadow:0 1px 3px rgba(0,0,0,.1);
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
@@ -540,7 +483,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
||||
|
||||
.stack-list{display:flex;flex-direction:column;gap:.5rem}
|
||||
.stack-card{background:var(--card-bg);border-radius:var(--radius);padding:1rem 1.25rem;box-shadow:var(--shadow);display:flex;justify-content:space-between;align-items:center;border-left:4px solid var(--gray)}
|
||||
.stack-state-green{border-left-color:var(--green)} .stack-state-red{border-left-color:var(--red)} .stack-state-yellow{border-left-color:var(--yellow)} .stack-state-gray{border-left-color:var(--gray)}
|
||||
.stack-state-green{border-left-color:var(--green)} .stack-state-red{border-left-color:var(--red)} .stack-state-yellow{border-left-color:var(--yellow)} .stack-state-orange{border-left-color:var(--orange)} .stack-state-gray{border-left-color:var(--gray)}
|
||||
.stack-info{display:flex;align-items:center;gap:.75rem}
|
||||
.stack-logo{width:32px;height:32px;border-radius:6px;object-fit:contain;background:#1c2128;padding:4px}
|
||||
.stack-logo-lg{width:48px;height:48px;border-radius:8px;object-fit:contain;background:#1c2128;padding:6px}
|
||||
@@ -549,14 +492,14 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
||||
|
||||
.stack-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(350px,1fr));gap:1rem}
|
||||
.stack-detail-card{background:var(--card-bg);border-radius:var(--radius);padding:1.25rem;box-shadow:var(--shadow);border-top:4px solid var(--gray)}
|
||||
.stack-detail-card.stack-state-green{border-top-color:var(--green)} .stack-detail-card.stack-state-red{border-top-color:var(--red)}
|
||||
.stack-detail-card.stack-state-green{border-top-color:var(--green)} .stack-detail-card.stack-state-red{border-top-color:var(--red)} .stack-detail-card.stack-state-orange{border-top-color:var(--orange)} .stack-detail-card.stack-state-yellow{border-top-color:var(--yellow)}
|
||||
.stack-detail-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.75rem}
|
||||
.stack-title-row{display:flex;align-items:center;gap:.75rem}
|
||||
.subdomain-link{font-size:.8rem;color:var(--blue);text-decoration:none} .subdomain-link:hover{text-decoration:underline}
|
||||
.stack-state-badge{padding:.2rem .6rem;border-radius:999px;font-size:.75rem;font-weight:600;white-space:nowrap}
|
||||
.state-green{background:var(--green-light);color:var(--green)} .state-red{background:var(--red-light);color:var(--red)}
|
||||
.state-yellow{background:var(--yellow-light);color:var(--yellow)} .state-gray{background:var(--gray-light);color:var(--gray)}
|
||||
.state-text-green{color:var(--green)} .state-text-red{color:var(--red)}
|
||||
.state-yellow{background:var(--yellow-light);color:var(--yellow)} .state-orange{background:var(--orange-light);color:var(--orange)} .state-gray{background:var(--gray-light);color:var(--gray)}
|
||||
.state-text-green{color:var(--green)} .state-text-red{color:var(--red)} .state-text-orange{color:var(--orange)} .state-text-yellow{color:var(--yellow)}
|
||||
.stack-detail-desc{color:var(--text-muted);font-size:.85rem;margin-bottom:.75rem}
|
||||
.stack-meta-badges{display:flex;flex-wrap:wrap;gap:.4rem;margin:.5rem 0}
|
||||
.meta-badge{background:var(--gray-light);color:var(--text-muted);padding:.15rem .5rem;border-radius:6px;font-size:.75rem}
|
||||
@@ -620,4 +563,4 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
||||
.stack-grid{grid-template-columns:1fr} .stats-grid{grid-template-columns:repeat(3,1fr)}
|
||||
.deploy-info{flex-direction:column}
|
||||
}
|
||||
`
|
||||
`
|
||||
Reference in New Issue
Block a user