Files
upaas/templates/templates.go
clawbot e1dc865226
All checks were successful
Check / check (push) Successful in 4s
feat: add webhook event history UI page (#164)
## Summary

Adds a per-app webhook event history page at `/apps/{id}/webhooks` showing received webhook events with match/no-match status.

## Changes

- **New template** `webhook_events.html` — displays webhook events in a table with time, event type, branch, commit SHA (linked when URL available), and match status badges
- **New handler** `HandleAppWebhookEvents()` in `webhook_events.go` — fetches app and its webhook events (limit 100)
- **New route** `GET /apps/{id}/webhooks` — registered in protected routes group
- **Template registration** — added `webhook_events.html` to the template cache in `templates.go`
- **Model enhancement** — added `ShortCommit()` method to `WebhookEvent` for truncated SHA display
- **App detail link** — added "Event History" link in the Webhook URL card on the app detail page

## UI

Follows the existing UI patterns (Tailwind CSS classes, Alpine.js `relativeTime`, badge styles, empty state, back-navigation). The page mirrors the deployments history page layout.

closes [#85](#85)

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #164
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-10 18:53:58 +01:00

100 lines
2.3 KiB
Go

// Package templates provides HTML template handling.
package templates
import (
"embed"
"fmt"
"html/template"
"io"
"sync"
)
//go:embed *.html
var templatesRaw embed.FS
// Template cache variables are global to enable efficient template reuse
// across requests without re-parsing on each call.
var (
//nolint:gochecknoglobals // singleton pattern for template cache
baseTemplate *template.Template
//nolint:gochecknoglobals // singleton pattern for template cache
pageTemplates map[string]*template.Template
//nolint:gochecknoglobals // protects template cache access
templatesMutex sync.RWMutex
)
// initTemplates parses base template and creates cloned templates for each page.
func initTemplates() {
templatesMutex.Lock()
defer templatesMutex.Unlock()
if pageTemplates != nil {
return
}
// Parse base template with shared components
baseTemplate = template.Must(template.ParseFS(templatesRaw, "base.html"))
// Pages that extend base
pages := []string{
"setup.html",
"login.html",
"dashboard.html",
"app_new.html",
"app_detail.html",
"app_edit.html",
"deployments.html",
"webhook_events.html",
}
pageTemplates = make(map[string]*template.Template)
for _, page := range pages {
// Clone base template and parse page-specific template into it
clone := template.Must(baseTemplate.Clone())
pageTemplates[page] = template.Must(clone.ParseFS(templatesRaw, page))
}
}
// GetParsed returns a template executor that routes to the correct page template.
func GetParsed() *TemplateExecutor {
initTemplates()
return &TemplateExecutor{}
}
// TemplateExecutor executes templates using the correct cloned template set.
type TemplateExecutor struct{}
// ExecuteTemplate executes the named template with the given data.
func (t *TemplateExecutor) ExecuteTemplate(
writer io.Writer,
name string,
data any,
) error {
templatesMutex.RLock()
tmpl, ok := pageTemplates[name]
templatesMutex.RUnlock()
if !ok {
// Fallback for non-page templates
err := baseTemplate.ExecuteTemplate(writer, name, data)
if err != nil {
return fmt.Errorf("execute base template %s: %w", name, err)
}
return nil
}
// Execute the "base" template from the cloned set
// (which has page-specific overrides)
err := tmpl.ExecuteTemplate(writer, "base", data)
if err != nil {
return fmt.Errorf("execute page template %s: %w", name, err)
}
return nil
}