Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func AdminHandler(w http.ResponseWriter, r *http.Request) {
<a href="/admin/api">API Log</a>
<a href="/admin/log">System Log</a>
<a href="/admin/env">Env Vars</a>
<a href="/admin/deploy">Deploy</a>
<a href="/admin/server">Server</a>
</div>`

html := app.RenderHTMLForRequest("Admin", "Admin Dashboard", content, r)
Expand Down
93 changes: 64 additions & 29 deletions admin/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func sourceDir() string {
return filepath.Join(home, "src", "mu")
}

// DeployHandler shows the deploy page and handles deploy requests
func DeployHandler(w http.ResponseWriter, r *http.Request) {
// UpdateHandler shows the update/restart page and handles requests
func UpdateHandler(w http.ResponseWriter, r *http.Request) {
_, _, err := auth.RequireAdmin(r)
if err != nil {
app.Forbidden(w, r, "Admin access required")
Expand All @@ -52,27 +52,32 @@ func DeployHandler(w http.ResponseWriter, r *http.Request) {
return
}

// GET — render deploy page
// GET — render server page
content := `<p><a href="/admin">← Admin</a></p>
<h2>Deploy</h2>
<p>Pull latest code, build, and restart the service.</p>
<h2>Server</h2>
<p><strong>Source:</strong> <code>` + sourceDir() + `</code></p>
<div id="deploy-controls">
<button id="deploy-btn" onclick="startDeploy()">Deploy</button>
<button id="update-btn" onclick="runAction('update')">Update</button>
<button id="restart-btn" onclick="runAction('restart')">Restart</button>
</div>
<pre id="deploy-output" style="background:#1e1e1e;color:#d4d4d4;padding:16px;border-radius:6px;min-height:200px;max-height:500px;overflow-y:auto;font-size:13px;line-height:1.6;white-space:pre-wrap;display:none;"></pre>
<script>
function startDeploy() {
var btn = document.getElementById('deploy-btn');
function runAction(action) {
var updateBtn = document.getElementById('update-btn');
var restartBtn = document.getElementById('restart-btn');
var output = document.getElementById('deploy-output');
btn.disabled = true;
btn.textContent = 'Deploying...';
var label = action === 'update' ? 'Updating...' : 'Restarting...';
updateBtn.disabled = true;
restartBtn.disabled = true;
if (action === 'update') updateBtn.textContent = label;
else restartBtn.textContent = label;
output.style.display = 'block';
output.textContent = '';

fetch('/admin/deploy', {
fetch('/admin/server', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: action})
}).then(function(res) { return res.json(); })
.then(function(data) {
var lines = '';
Expand All @@ -84,23 +89,28 @@ func DeployHandler(w http.ResponseWriter, r *http.Request) {
lines += entry.output + '\n';
}
});
var doneLabel = action === 'update' ? 'Update' : 'Restart';
if (data.success) {
lines += '\n<span style="color:#6a9955;font-weight:bold;">Deploy complete. Restarting...</span>\n';
lines += '\n<span style="color:#6a9955;font-weight:bold;">' + doneLabel + ' complete. Restarting...</span>\n';
} else {
lines += '\n<span style="color:#f44747;font-weight:bold;">Deploy failed.</span>\n';
lines += '\n<span style="color:#f44747;font-weight:bold;">' + doneLabel + ' failed.</span>\n';
}
output.innerHTML = lines;
btn.disabled = false;
btn.textContent = 'Deploy';
updateBtn.disabled = false;
restartBtn.disabled = false;
updateBtn.textContent = 'Update';
restartBtn.textContent = 'Restart';
}).catch(function(err) {
output.innerHTML = '<span style="color:#f44747;">Error: ' + err.message + '</span>';
btn.disabled = false;
btn.textContent = 'Deploy';
updateBtn.disabled = false;
restartBtn.disabled = false;
updateBtn.textContent = 'Update';
restartBtn.textContent = 'Restart';
});
}
</script>`

html := app.RenderHTMLForRequest("Admin", "Deploy", content, r)
html := app.RenderHTMLForRequest("Admin", "Server", content, r)
w.Write([]byte(html))
}

Expand All @@ -111,7 +121,7 @@ func handleDeploy(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"logs": []deployLogEntry{{Step: "lock", Output: "Deploy already in progress", Success: false}},
"logs": []deployLogEntry{{Step: "lock", Output: "Already in progress", Success: false}},
})
return
}
Expand All @@ -124,22 +134,37 @@ func handleDeploy(w http.ResponseWriter, r *http.Request) {
deployMu.Unlock()
}()

var req struct {
Action string `json:"action"`
}
json.NewDecoder(r.Body).Decode(&req)

dir := sourceDir()
var logs []deployLogEntry
success := true

steps := []struct {
type step struct {
name string
cmd string
args []string
}{
{"git pull", "git", []string{"pull", "origin", "main"}},
{"go install", "go", []string{"install"}},
{"restart service", "sudo", []string{"-n", "systemctl", "restart", "mu"}},
}

for _, step := range steps {
entry := runStep(dir, step.name, step.cmd, step.args)
var steps []step
switch req.Action {
case "restart":
steps = []step{
{"restart service", "sudo", []string{"-n", "systemctl", "restart", "mu"}},
}
default: // "update"
steps = []step{
{"git pull", "git", []string{"pull", "origin", "main"}},
{"go install", "go", []string{"install"}},
{"restart service", "sudo", []string{"-n", "systemctl", "restart", "mu"}},
}
}

for _, s := range steps {
entry := runStep(dir, s.name, s.cmd, s.args)
logs = append(logs, entry)
if !entry.Success {
success = false
Expand All @@ -163,8 +188,18 @@ func runStep(dir, name, cmdName string, args []string) deployLogEntry {
cmd := exec.Command(cmdName, args...)
cmd.Dir = dir

// Inherit env so go build picks up GOPATH etc
cmd.Env = append(os.Environ(), "HOME="+os.Getenv("HOME"))
// Inherit env and ensure Go/snap paths are available
home := os.Getenv("HOME")
path := os.Getenv("PATH")
goPath := filepath.Join(home, "go", "bin")
goRoot := "/usr/local/go/bin"
if !strings.Contains(path, goPath) {
path = goPath + ":" + path
}
if !strings.Contains(path, goRoot) {
path = goRoot + ":" + path
}
cmd.Env = append(os.Environ(), "HOME="+home, "PATH="+path)

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
Expand Down
6 changes: 3 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func main() {
"/admin/api": true,
"/admin/log": true,
"/admin/env": true,
"/admin/deploy": true,
"/admin/server": true,
"/plans": false, // Public - shows pricing options
"/donate": false,
"/wallet": false, // Public - shows wallet info; auth checked in handler
Expand Down Expand Up @@ -281,8 +281,8 @@ func main() {
// environment variables status
http.HandleFunc("/admin/env", admin.EnvHandler)

// deploy
http.HandleFunc("/admin/deploy", admin.DeployHandler)
// server update and restart
http.HandleFunc("/admin/server", admin.UpdateHandler)

// plans page (public - overview of options)
http.HandleFunc("/plans", app.Plans)
Expand Down