180 lines
4.7 KiB
Go
180 lines
4.7 KiB
Go
package webui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/a-h/templ"
|
|
|
|
"satoru/internal/store"
|
|
)
|
|
|
|
type RuntimeCheck struct {
|
|
Name string
|
|
Installed bool
|
|
Details string
|
|
}
|
|
|
|
type WorkflowStage struct {
|
|
Title string
|
|
Description string
|
|
}
|
|
|
|
type DashboardData struct {
|
|
Now time.Time
|
|
User store.User
|
|
Sites []store.Site
|
|
RuntimeChecks []RuntimeCheck
|
|
WorkflowStages []WorkflowStage
|
|
FlashMessage string
|
|
}
|
|
|
|
func Dashboard(data DashboardData) templ.Component {
|
|
return templ.ComponentFunc(func(_ context.Context, w io.Writer) error {
|
|
role := "Operator"
|
|
if data.User.IsAdmin {
|
|
role = "Admin"
|
|
}
|
|
|
|
var checks strings.Builder
|
|
for _, c := range data.RuntimeChecks {
|
|
status := "missing"
|
|
if c.Installed {
|
|
status = "installed"
|
|
}
|
|
checks.WriteString(fmt.Sprintf(`<li class="status %s"><strong>%s</strong><span>%s</span></li>`,
|
|
status,
|
|
html.EscapeString(c.Name),
|
|
html.EscapeString(c.Details)))
|
|
}
|
|
|
|
var flows strings.Builder
|
|
for _, s := range data.WorkflowStages {
|
|
flows.WriteString(fmt.Sprintf(`<li class="workflow-step"><strong>%s</strong><p>%s</p></li>`,
|
|
html.EscapeString(s.Title),
|
|
html.EscapeString(s.Description)))
|
|
}
|
|
|
|
var sites strings.Builder
|
|
if len(data.Sites) == 0 {
|
|
sites.WriteString(`<p class="muted">No sites added yet. Add your first Linux SSH site below.</p>`)
|
|
}
|
|
for _, site := range data.Sites {
|
|
last := "Never run"
|
|
if site.LastRunAt.Valid {
|
|
last = site.LastRunAt.Time.Local().Format(time.RFC1123)
|
|
}
|
|
runStatus := "pending"
|
|
if site.LastRunStatus.Valid {
|
|
runStatus = site.LastRunStatus.String
|
|
}
|
|
runOutput := "(no output yet)"
|
|
if site.LastRunOutput.Valid && strings.TrimSpace(site.LastRunOutput.String) != "" {
|
|
runOutput = site.LastRunOutput.String
|
|
}
|
|
|
|
sites.WriteString(fmt.Sprintf(`
|
|
<article class="site-card">
|
|
<div class="site-head">
|
|
<h3>%s@%s</h3>
|
|
<form method="post" action="/sites/%d/run">
|
|
<button class="button" type="submit">Run</button>
|
|
</form>
|
|
</div>
|
|
<p class="muted">Path: <code>%s</code></p>
|
|
<p class="muted">Last run: %s</p>
|
|
<p class="muted">Status: <span class="pill %s">%s</span></p>
|
|
<pre class="output">%s</pre>
|
|
</article>`,
|
|
html.EscapeString(site.SSHUser),
|
|
html.EscapeString(site.Host),
|
|
site.ID,
|
|
html.EscapeString(site.RemotePath),
|
|
html.EscapeString(last),
|
|
html.EscapeString(runStatus),
|
|
html.EscapeString(runStatus),
|
|
html.EscapeString(runOutput),
|
|
))
|
|
}
|
|
|
|
flash := ""
|
|
if data.FlashMessage != "" {
|
|
flash = fmt.Sprintf(`<p class="notice">%s</p>`, html.EscapeString(formatFlash(data.FlashMessage)))
|
|
}
|
|
|
|
_, err := io.WriteString(w, fmt.Sprintf(`<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Satoru Dashboard</title>
|
|
<link rel="stylesheet" href="/static/app.css" />
|
|
</head>
|
|
<body>
|
|
<main class="card dashboard-card">
|
|
<div class="row split">
|
|
<div>
|
|
<p class="eyebrow">Satoru</p>
|
|
<h1>Managed Sites Overview</h1>
|
|
<p class="muted">Signed in as <strong>%s</strong> (%s).</p>
|
|
</div>
|
|
<div class="row">
|
|
<a class="button ghost" href="/account/password">Change password</a>
|
|
<form method="post" action="/signout"><button class="button ghost" type="submit">Sign out</button></form>
|
|
</div>
|
|
</div>
|
|
%s
|
|
<section>
|
|
<h2>Host Runtime Status</h2>
|
|
<ul class="status-list">%s</ul>
|
|
</section>
|
|
<section>
|
|
<h2>Planned Workflow</h2>
|
|
<ol class="workflow">%s</ol>
|
|
</section>
|
|
<section>
|
|
<h2>Add Site</h2>
|
|
<form class="grid-3" method="post" action="/sites">
|
|
<label class="stack"><span>SSH User</span><input name="ssh_user" required /></label>
|
|
<label class="stack"><span>Server</span><input name="host" placeholder="host or ip" required /></label>
|
|
<label class="stack"><span>Path</span><input name="remote_path" placeholder="/var/www" required /></label>
|
|
<button class="button" type="submit">Add site</button>
|
|
</form>
|
|
</section>
|
|
<section>
|
|
<h2>Managed Sites</h2>
|
|
%s
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>`,
|
|
html.EscapeString(data.User.Username),
|
|
html.EscapeString(role),
|
|
flash,
|
|
checks.String(),
|
|
flows.String(),
|
|
sites.String(),
|
|
))
|
|
return err
|
|
})
|
|
}
|
|
|
|
func formatFlash(code string) string {
|
|
switch code {
|
|
case "site-added":
|
|
return "Site added."
|
|
case "site-ran":
|
|
return "Run completed."
|
|
case "site-invalid":
|
|
return "All site fields are required."
|
|
case "password-updated":
|
|
return "Password updated."
|
|
default:
|
|
return code
|
|
}
|
|
}
|