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
12 changes: 6 additions & 6 deletions internal/webui/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,16 @@ var templatesFS embed.FS
// "content", and "scripts" blocks without colliding.
var pageTemplates = []string{
"templates/run_detail.html",
"templates/personas.html",
"templates/persona_detail.html",
"templates/pipeline_detail.html",
"templates/contracts.html",
"templates/contract_detail.html",
"templates/skills.html",
"templates/skill_detail.html",
"templates/compose.html",
"templates/issues.html",
"templates/issue_detail.html",
"templates/prs.html",
"templates/pr_detail.html",
"templates/health.html",
"templates/analytics.html",
"templates/retros.html",
"templates/notfound.html",
"templates/compare.html",
"templates/webhooks.html",
"templates/webhook_detail.html",
Expand All @@ -62,6 +56,12 @@ var standalonePageTemplates = []string{
"templates/proposals/detail.html",
"templates/runs.html",
"templates/pipelines.html",
"templates/issues.html",
"templates/prs.html",
"templates/health.html",
"templates/contracts.html",
"templates/personas.html",
"templates/notfound.html",
}

// parseTemplates parses all embedded HTML templates using a clone-per-page
Expand Down
2 changes: 1 addition & 1 deletion internal/webui/handlers_contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (s *Server) handleContractsPage(w http.ResponseWriter, r *http.Request) {
}

w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.assets.templates["templates/contracts.html"].ExecuteTemplate(w, "templates/layout.html", data); err != nil {
if err := s.assets.templates["templates/contracts.html"].Execute(w, data); err != nil {
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/webui/handlers_health.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (s *Server) handleHealthPage(w http.ResponseWriter, r *http.Request) {
}

w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.assets.templates["templates/health.html"].ExecuteTemplate(w, "templates/layout.html", data); err != nil {
if err := s.assets.templates["templates/health.html"].Execute(w, data); err != nil {
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/webui/handlers_issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (s *Server) handleIssuesPage(w http.ResponseWriter, r *http.Request) {
}

w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.assets.templates["templates/issues.html"].ExecuteTemplate(w, "templates/layout.html", data); err != nil {
if err := s.assets.templates["templates/issues.html"].Execute(w, data); err != nil {
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/webui/handlers_personas.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (s *Server) handlePersonasPage(w http.ResponseWriter, r *http.Request) {
}

w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.assets.templates["templates/personas.html"].ExecuteTemplate(w, "templates/layout.html", data); err != nil {
if err := s.assets.templates["templates/personas.html"].Execute(w, data); err != nil {
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/webui/handlers_prs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (s *Server) handlePRsPage(w http.ResponseWriter, r *http.Request) {
}

w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.assets.templates["templates/prs.html"].ExecuteTemplate(w, "templates/layout.html", data); err != nil {
if err := s.assets.templates["templates/prs.html"].Execute(w, data); err != nil {
log.Printf("[webui] template error rendering prs page: %v", err)
http.Error(w, "template error", http.StatusInternalServerError)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/webui/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,6 @@ func (s *Server) handleNotFound(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
tmpl := s.assets.templates["templates/notfound.html"]
if tmpl != nil {
_ = tmpl.ExecuteTemplate(w, "templates/layout.html", nil)
_ = tmpl.Execute(w, nil)
}
}
116 changes: 76 additions & 40 deletions internal/webui/templates/contracts.html
Original file line number Diff line number Diff line change
@@ -1,43 +1,79 @@
{{define "title"}}Contracts · Wave{{end}}
{{define "content"}}
<div class="w-head"><h1>Contracts</h1></div>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Contracts &mdash; Wave</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
{{if csrfToken}}<meta name="csrf-token" content="{{csrfToken}}">{{end}}
</head>
<body>
<nav class="w-nav">
<a href="/" class="w-nav-brand">
<svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" aria-hidden="true">
<path d="M2 14 C6 6, 10 6, 14 14 C18 22, 22 22, 26 14"/>
<path d="M2 14 C6 22, 10 22, 14 14 C18 6, 22 6, 26 14" opacity="0.35"/>
</svg>
Wave
</a>
<div class="w-nav-links">
<a href="/work">Work</a>
<a href="/runs">Runs</a>
<a href="/pipelines">Pipelines</a>
<a href="/proposals">Proposals</a>
<a href="/issues">Issues</a>
<a href="/prs">PRs</a>
<a href="/onboard">Onboard</a>
<a href="/health" style="margin-left: auto;">Health</a>
</div>
</nav>

{{if .Contracts}}
<div class="wr-toolbar">
<div class="wr-controls">
<input type="text" id="contract-search" class="wr-search" placeholder="Search contracts..." oninput="filterItems(this.value)">
<span class="wr-count">{{len .Contracts}} contracts</span>
<div class="w-container">
<div class="w-page-header">
<h1 class="w-page-title">Contracts</h1>
</div>
</div>
<div class="wr-list">
{{range .Contracts}}
<a href="/contracts/{{.Name}}" class="wr-run">
<div class="wr-accent st-defined"></div>
<div class="wr-body">
<div class="wr-row1">
<span class="wr-name">{{.Name}}</span>
{{if .Title}}<span class="ct-title">{{.Title}}</span>{{end}}
</div>
{{if .Description}}<div class="wr-row2"><span class="ct-desc">{{.Description}}</span></div>{{end}}
</div>
<div class="wr-meta">
<span class="wr-date ct-file">{{.Filename}}</span>

{{if .Contracts}}
<div class="w-filterbar" style="border-radius: 8px; border-bottom: 1px solid var(--color-border);">
<div style="margin-left: auto; display: flex; gap: 8px; align-items: center;">
<input type="text" id="contract-search" placeholder="Search contracts..." style="background: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text-secondary); padding: 6px 10px; border-radius: 6px; font-size: 12px; width: 180px; font-family: inherit;" oninput="filterItems(this.value)">
<span style="font-size: 12px; color: var(--color-text-secondary);">{{len .Contracts}} contracts</span>
</div>
</div>

<div class="w-list standalone" id="contract-list">
{{range .Contracts}}
<a href="/contracts/{{.Name}}" class="run-card" style="display: flex; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--color-border-light); text-decoration: none; color: inherit;" onmouseover="this.style.background='var(--color-bg-tertiary)'" onmouseout="this.style.background=''">
<div style="width: 4px; border-radius: 2px; background: var(--color-link); flex-shrink: 0;"></div>
<div style="flex: 1; min-width: 0;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-weight: 500;">{{.Name}}</span>
{{if .Title}}<span style="color: var(--color-text-secondary); font-size: 12px;">{{.Title}}</span>{{end}}
</div>
{{if .Description}}<div style="color: var(--color-text-secondary); font-size: 12px; margin-top: 2px;">{{.Description}}</div>{{end}}
</div>
</a>
{{end}}
</div>
{{else}}
<div class="wr-empty">No contracts found. Add schemas to <code>.agents/contracts/</code></div>
{{end}}
{{end}}
{{define "scripts"}}
<script>
function filterItems(query) {
var q = query.toLowerCase().trim();
document.querySelectorAll('.wr-list > a.wr-run').forEach(function(card) {
if (!q) { card.style.display = ''; return; }
card.style.display = card.textContent.toLowerCase().indexOf(q) !== -1 ? '' : 'none';
});
}
</script>
{{end}}
<span style="font-size: 12px; color: var(--color-text-muted); flex-shrink: 0;">{{.Filename}}</span>
</a>
{{end}}
</div>
{{else}}
<div class="w-empty">
<h3>No contracts found</h3>
<p style="color: var(--color-text-secondary); font-size: 13px; margin-top: 4px;">Add schemas to <code style="background: var(--color-bg-secondary); padding: 2px 6px; border-radius: 4px;">.agents/contracts/</code></p>
</div>
{{end}}
</div>

<script src="/static/app.js"></script>
<script>
function filterItems(query) {
var q = query.toLowerCase().trim();
document.querySelectorAll('#contract-list > a.run-card').forEach(function(card) {
if (!q) { card.style.display = ''; return; }
card.style.display = card.textContent.toLowerCase().indexOf(q) !== -1 ? '' : 'none';
});
}
</script>
</body>
</html>
112 changes: 76 additions & 36 deletions internal/webui/templates/health.html
Original file line number Diff line number Diff line change
@@ -1,42 +1,82 @@
{{define "title"}}Health · Wave{{end}}
{{define "content"}}
<div class="page-header">
<h1>Health Checks</h1>
<button class="btn" onclick="location.reload()">Re-run Checks</button>
</div>
{{if .Checks}}
<div class="health-checks">
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Health &mdash; Wave</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
{{if csrfToken}}<meta name="csrf-token" content="{{csrfToken}}">{{end}}
</head>
<body>
<nav class="w-nav">
<a href="/" class="w-nav-brand">
<svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" aria-hidden="true">
<path d="M2 14 C6 6, 10 6, 14 14 C18 22, 22 22, 26 14"/>
<path d="M2 14 C6 22, 10 22, 14 14 C18 6, 22 6, 26 14" opacity="0.35"/>
</svg>
Wave
</a>
<div class="w-nav-links">
<a href="/work">Work</a>
<a href="/runs">Runs</a>
<a href="/pipelines">Pipelines</a>
<a href="/proposals">Proposals</a>
<a href="/issues">Issues</a>
<a href="/prs">PRs</a>
<a href="/onboard">Onboard</a>
<a href="/health" class="active" style="margin-left: auto;">Health</a>
</div>
</nav>

<div class="w-container">
<div class="w-page-header" style="display: flex; align-items: center; justify-content: space-between;">
<h1 class="w-page-title">Health Checks</h1>
<button class="w-btn" onclick="location.reload()">Re-run Checks</button>
</div>

{{if .Checks}}
<div style="display: grid; gap: 12px;">
{{range .Checks}}
<div class="card health-card health-{{.Status}}">
<div class="health-header">
<span class="health-icon">
{{if eq .Status "ok"}}&#10003;{{else if eq .Status "warn"}}&#9888;{{else}}&#10007;{{end}}
</span>
<h3>{{.Name}}</h3>
<span class="badge badge-{{.Status}}">{{.Status}}</span>
<div style="border: 1px solid var(--color-border); border-radius: 8px; overflow: hidden;">
<div style="display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--color-border-light);">
<span style="font-size: 16px;">
{{if eq .Status "ok"}}<span style="color: var(--color-completed);">&#10003;</span>
{{else if eq .Status "warn"}}<span style="color: var(--color-completed-empty);">&#9888;</span>
{{else}}<span style="color: var(--color-failed);">&#10007;</span>{{end}}
</span>
<span style="font-weight: 500; flex: 1;">{{.Name}}</span>
{{if eq .Status "ok"}}<span class="badge badge-green">ok</span>
{{else if eq .Status "warn"}}<span class="badge badge-yellow">warn</span>
{{else}}<span class="badge badge-red">error</span>{{end}}
</div>
<p class="health-message">{{.Message}}</p>
{{if .Message}}
<div style="padding: 8px 16px; font-size: 13px; color: var(--color-text-secondary);">{{.Message}}</div>
{{end}}
{{if .Details}}
<details class="health-details">
<summary>Details</summary>
<table class="details-table">
{{range $key, $value := .Details}}
<tr>
<td><strong>{{$key}}</strong></td>
<td>{{$value}}</td>
</tr>
{{end}}
</table>
<details style="padding: 0 16px 12px;">
<summary style="cursor: pointer; font-size: 12px; color: var(--color-text-secondary); padding: 4px 0;">Details</summary>
<table style="width: 100%; font-size: 12px; margin-top: 8px;">
{{range $key, $value := .Details}}
<tr>
<td style="padding: 4px 0; color: var(--color-text-secondary); font-weight: 500; width: 40%;">{{$key}}</td>
<td style="padding: 4px 0;">{{$value}}</td>
</tr>
{{end}}
</table>
</details>
{{end}}
</div>
</div>
{{end}}
</div>
{{else}}
<div class="empty-state">
<div class="empty-state-icon">&#9825;</div>
<p><strong>No health checks available</strong></p>
<p>Run <code>wave doctor</code> to check project health.</p>
</div>
{{end}}
{{end}}
</div>
{{else}}
<div class="w-empty">
<h3>No health checks available</h3>
<p style="color: var(--color-text-secondary); font-size: 13px; margin-top: 4px;">Run <code style="background: var(--color-bg-secondary); padding: 2px 6px; border-radius: 4px;">wave doctor</code> to check project health.</p>
</div>
{{end}}
</div>

<script src="/static/app.js"></script>
</body>
</html>
Loading
Loading