Proxy Datasette via an iframe

Signed-off-by: AKP <tom@tdpain.net>
This commit is contained in:
akp 2023-04-03 14:03:04 +01:00
parent a95f759ddd
commit 846813120b
No known key found for this signature in database
GPG key ID: AA5726202C8879B7
22 changed files with 122 additions and 783 deletions

View file

@ -1,6 +1,9 @@
package config
import "github.com/codemicro/analytics/analytics/config/internal/debug"
import (
"fmt"
"github.com/codemicro/analytics/analytics/config/internal/debug"
)
var Debug = debug.Enable
@ -14,6 +17,9 @@ type Config struct {
HTTP struct {
Address string
}
Datasette struct {
Address string
}
}
func Load() (*Config, error) {
@ -22,10 +28,13 @@ func Load() (*Config, error) {
return nil, err
}
fmt.Println(cl.rawConfigFileContents)
conf := new(Config)
conf.Ingest.Address = asString(cl.withDefault("ingest.address", "127.0.0.1:7500"))
conf.HTTP.Address = asString(cl.withDefault("http.address", "127.0.0.1:8080"))
conf.Database.DSN = asString(cl.withDefault("database.dsn", "analytics.db"))
conf.Datasette.Address = asString(cl.withDefault("datasette.address", "127.0.0.1:8001"))
return conf, nil
}

View file

@ -0,0 +1,70 @@
package httpsrv
import (
"github.com/codemicro/analytics/analytics/config"
"github.com/codemicro/analytics/analytics/db"
"github.com/flosch/pongo2/v6"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy"
"github.com/rs/zerolog/log"
"strings"
"time"
)
type WebUI struct {
conf *config.Config
db *db.DB
app *fiber.App
templates *pongo2.TemplateSet
}
func Start(conf *config.Config, db *db.DB) *WebUI {
wui := &WebUI{
conf: conf,
db: db,
}
wui.app = fiber.New(fiber.Config{
DisableStartupMessage: !config.Debug,
})
wui.registerHandlers()
//wui.templates = pongo2.NewSet("templates", templates.TemplateLoader())
go func() {
if err := wui.app.Listen(conf.HTTP.Address); err != nil {
log.Error().Err(err).Msg("HTTP server listen failed")
return
}
}()
log.Info().Msgf("HTTP server alive on %s", conf.HTTP.Address)
return wui
}
func (wui *WebUI) sendTemplate(ctx *fiber.Ctx, fname string, renderCtx pongo2.Context) error {
tpl, err := wui.templates.FromFile(fname)
if err != nil {
return err
}
res, err := tpl.ExecuteBytes(renderCtx)
if err != nil {
return err
}
ctx.Type("html")
return ctx.Send(res)
}
func (wui *WebUI) Stop() error {
return wui.app.ShutdownWithTimeout(time.Second * 5)
}
func (wui *WebUI) registerHandlers() {
wui.app.Get("/", wui.index)
wui.app.Use("/ds", func(ctx *fiber.Ctx) error {
path := ctx.Path()
path = strings.TrimPrefix(path, ctx.Route().Path)
return proxy.Do(ctx, wui.conf.Datasette.Address+path)
})
}

View file

@ -0,0 +1,14 @@
package httpsrv
import (
_ "embed"
"github.com/gofiber/fiber/v2"
)
//go:embed index.html
var indexPage []byte
func (wui *WebUI) index(ctx *fiber.Ctx) error {
ctx.Set(fiber.HeaderContentType, "text/html")
return ctx.Send(indexPage)
}

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Analytics</title>
<style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
iframe#main {
border: none;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<iframe src="/ds" id="main"></iframe>
</body>
</html>

View file

@ -3,8 +3,8 @@ package main
import (
"github.com/codemicro/analytics/analytics/config"
"github.com/codemicro/analytics/analytics/db"
"github.com/codemicro/analytics/analytics/httpsrv"
"github.com/codemicro/analytics/analytics/ingest"
"github.com/codemicro/analytics/analytics/webui"
"github.com/rs/zerolog/log"
"os"
"os/signal"
@ -33,7 +33,7 @@ func run() error {
return err
}
http := webui.Start(conf, database)
http := httpsrv.Start(conf, database)
waitForSignal(syscall.SIGINT)

View file

@ -1,104 +0,0 @@
package webui
import (
"context"
"fmt"
"github.com/flosch/pongo2/v6"
"github.com/gofiber/fiber/v2"
"strconv"
)
func (wui *WebUI) page_index(ctx *fiber.Ctx) error {
return wui.sendTemplate(ctx, "index.html", nil)
}
func (wui *WebUI) partial_activeSessionsTable(ctx *fiber.Ctx) error {
ht := &HTMLTable{
Path: "/partial/activeSessions",
Headers: []*HTMLTableHeader{
{"", "", false, true},
{"User agent", "", false, false},
{"IP", "", false, false},
{"Last seen", "last_seen", true, false},
},
Data: func(sortKey, sortDirection string) ([][]any, error) {
var sort string
if !(sortKey == "" || sortDirection == "") {
sort = sortKey + " " + sortDirection
}
sessions, err := wui.db.GetSessionsWithActivityAfter(30, sort)
if err != nil {
return nil, err
}
var res [][]any
for _, sess := range sessions {
ua, _ := pongo2.ApplyFilter("truncatechars", pongo2.AsValue(sess.UserAgent), pongo2.AsValue(40))
ua, _ = pongo2.ApplyFilter("default", ua, unsetValue)
res = append(res, []any{
fmt.Sprintf(`<a href="/session?id=%s">[Link]</a>`, sess.ID),
ua,
getValue(pongo2.ApplyFilter("truncatechars", pongo2.AsValue(sess.IPAddr), pongo2.AsValue(30))),
getValue(pongo2.ApplyFilter("shortTimeSince", pongo2.AsValue(sess.LastSeen), nil)),
})
}
return res, nil
},
DefaultSortKey: "last_seen",
DefaultSortDirection: "desc",
ShowNumberOfEntries: true,
}
return wui.renderHTMLTable(ctx, ht)
}
func (wui *WebUI) partial_topURLs(ctx *fiber.Ctx) error {
nStr := ctx.Query("n")
var n int
if nStr != "" {
n, _ = strconv.Atoi(nStr)
}
hoursStr := ctx.Query("hours")
var hours int
if hoursStr != "" {
hours, _ = strconv.Atoi(hoursStr)
}
ht := &HTMLTable{
Headers: []*HTMLTableHeader{
{"Count", "", false, false},
{"Host", "", false, false},
{"Path", "", false, false},
},
Data: func(sortKey, sortDirection string) ([][]any, error) {
var counts []struct {
Host string
URI string
Count int
}
q := wui.db.DB.NewSelect().
ColumnExpr(`"host", "uri", COUNT(*) as "count"`).
Table("requests").
GroupExpr(`"host", "uri"`).
OrderExpr(`"count" DESC`)
if n > 0 {
q = q.Limit(n)
}
if hours > 0 {
q = q.Where(fmt.Sprintf(`datetime() < datetime(time, '+%d hours')`, hours))
}
if err := q.Scan(context.Background(), &counts); err != nil {
return nil, err
}
var res [][]any
for _, c := range counts {
res = append(res, []any{c.Count, c.Host, c.URI})
}
return res, nil
},
}
return wui.renderHTMLTable(ctx, ht)
}

View file

@ -1,21 +0,0 @@
<p>{{ sessions|length }} active session{% if sessions|length != 0%}s{% endif %}</p>
{% if sessions|length != 0 %}
<div class="table">
<table>
<tr>
<th></th>
<th>User agent</th>
<th>IP</th>
<th>Last seen</th>
</tr>
{% for session in sessions %}
<tr>
<td><a href="/session?id={{ session.ID }}">[Link]</a></td>
<td title="{{ session.UserAgent }}">{{ session.UserAgent | truncatechars:40 }}</td>
<td title="{{ session.IPAddr }}">{{ session.IPAddr | truncatechars:30 }}</td>
<td>{{ session.LastSeen | shortTimeSince }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}

View file

@ -1,31 +0,0 @@
<div class="table" hx-target="this">
<table>
<tr>
{% for header in headers %}
<th>
{% if header.Sortable %}
<a href="#" hx-get="{{ path }}?sortKey={{ header.Slug }}{% if sortKey and header.Slug == sortKey %}&sortDir={% if sortDirection == "asc" %}desc{% elif sortDirection == "desc" %}asc{% endif %}{% endif %}">
{% endif %}
{{ header.Name }}{% if sortKey %}{% if header.Slug == sortKey %} {% if sortDirection == "asc" %}▲{% else %}▼{% endif %}{% endif %}{% endif %}
{% if header.Sortable %}</a>{% endif %}
</th>
{% endfor %}
</tr>
{% for row in rows %}
<tr>
{% for item in row %}
{% with headers[forloop.Counter - 1] as header %}
{% if header.Safe %}
<td>{{ item | safe }}</td>
{% else %}
<td>{{ item }}</td>
{% endif %}
{% endwith %}
{% endfor %}
</tr>
{% endfor %}
</table>
{% if showNumberOfEntries %}
<span style="margin-top: 5px" class="italic">{{ rows | length }} records</span>
{% endif %}
</div>

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="stylesheet" href="/assets/css/main.css">
<script src="https://unpkg.com/htmx.org@1.8.6"></script>
{% block head %}{% endblock%}
</head>
<body>
<main>
<h1>Analytics</h1>
{% block main %}{% endblock %}
</main>
</body>
</html>

View file

@ -1,23 +0,0 @@
{% extends "extendable/base.html" %}
{% block main %}
<h2>Top 10 URLs (past 24 hours)</h2>
<div hx-get="/partial/topURLs?n=10&hours=24"
hx-trigger="load"
hx-swap="outerHTML"
>
Loading...
</div>
<h2>Active sessions</h2>
<div hx-get="/partial/activeSessions?minutes=30"
hx-trigger="load"
hx-swap="outerHTML"
>
Loading...
</div>
<a href="/sessions">[See all]</a>
{% endblock %}

View file

@ -1,14 +0,0 @@
{% extends "extendable/base.html" %}
{% block main %}
<a href="/">[Home]</a>
<h2>All sessions</h2>
<div hx-get="/partial/listSessions"
hx-trigger="load"
hx-swap="outerHTML"
>
Loading...
</div>
{% endblock %}

View file

@ -1,31 +0,0 @@
package templates
import (
"embed"
"github.com/flosch/pongo2/v6"
"io"
"path"
)
//go:embed *
var templateFS embed.FS
func TemplateLoader() pongo2.TemplateLoader {
return &templateLoader{templateFS}
}
type templateLoader struct {
embed.FS
}
func (tl *templateLoader) Abs(base, name string) string {
return path.Join(path.Dir(base), name)
}
func (tl *templateLoader) Get(name string) (io.Reader, error) {
f, err := tl.Open(name)
if err != nil {
return nil, err
}
return f, nil
}

View file

@ -1,20 +0,0 @@
{% extends "extendable/base.html" %}
{% block main %}
<a href="/">[Home]</a>
<h2>Logs from session</h2>
<p>
Session ID: {{ session.ID }}<br>
IP address: {{ session.IPAddr }}<br>
User agent: {% if session.UserAgent %}{{ session.UserAgent }}{% else %}<span class="italic">unset</span>{% endif %}
</p>
<div hx-get="/partial/sessionLogs/{{ session.ID }}"
hx-trigger="load"
hx-swap="outerHTML"
>
Loading...
</div>
{% endblock %}

View file

@ -1,20 +0,0 @@
{% extends "extendable/base.html" %}
{% block main %}
<a href="/">[Home]</a>
<h2>Request detail</h2>
<p>
ID: {{ request.ID }}<br>
Time: {{ request.Time | date:"2006-01-02 15:04:05" }}<br>
IP address: {{ request.IPAddr }}<br>
Host: {{ request.Host }}<br>
Raw path: {{ request.RawURI }}<br>
Normalised path: {{ request.URI }}<br>
Referer: {% if request.Referer %}{{ request.Referer }}{% else %}<span class="italic">unset</span>{% endif %}<br>
User agent: {% if request.UserAgent %}{{ request.UserAgent }}{% else %}<span class="italic">unset</span>{% endif %}<br>
Status code: {{ request.StatusCode }}<br>
Session: <a href="/session?id={{ request.SessionID }}">{{ request.SessionID }}</a>
</p>
{% endblock %}

View file

@ -1,51 +0,0 @@
package webui
import (
"fmt"
"github.com/flosch/pongo2/v6"
"github.com/gofiber/fiber/v2"
)
func (wui *WebUI) page_listSessions(ctx *fiber.Ctx) error {
return wui.sendTemplate(ctx, "list-sessions.html", nil)
}
func (wui *WebUI) partial_listSessions(ctx *fiber.Ctx) error {
ht := &HTMLTable{
Path: "/partial/listSessions",
Headers: []*HTMLTableHeader{
{"", "", false, true},
{"User agent", "", false, false},
{"IP", "", false, false},
{"Last seen", "last_seen", true, false},
},
Data: func(sortKey, sortDirection string) ([][]any, error) {
var sort string
if !(sortKey == "" || sortDirection == "") {
sort = sortKey + " " + sortDirection
}
sessions, err := wui.db.GetSessionsWithActivityAfter(60*24, sort)
if err != nil {
return nil, err
}
var res [][]any
for _, sess := range sessions {
ua, _ := pongo2.ApplyFilter("truncatechars", pongo2.AsValue(sess.UserAgent), pongo2.AsValue(40))
ua, _ = pongo2.ApplyFilter("default", ua, unsetValue)
res = append(res, []any{
fmt.Sprintf(`<a href="/session?id=%s">[Link]</a>`, sess.ID),
ua,
getValue(pongo2.ApplyFilter("truncatechars", pongo2.AsValue(sess.IPAddr), pongo2.AsValue(30))),
getValue(pongo2.ApplyFilter("shortTimeSince", pongo2.AsValue(sess.LastSeen), nil)),
})
}
return res, nil
},
DefaultSortKey: "last_seen",
DefaultSortDirection: "desc",
ShowNumberOfEntries: true,
}
return wui.renderHTMLTable(ctx, ht)
}

View file

@ -1,30 +0,0 @@
package webui
import (
"context"
"database/sql"
"errors"
"github.com/codemicro/analytics/analytics/db/models"
"github.com/flosch/pongo2/v6"
"github.com/gofiber/fiber/v2"
)
func (wui *WebUI) page_requestDetail(ctx *fiber.Ctx) error {
id := ctx.Query("id")
if id == "" {
return fiber.ErrBadRequest
}
request := new(models.Request)
if err := wui.db.DB.NewSelect().Model(request).Where("id = ?", id).Scan(context.Background(), request); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fiber.ErrNotFound
}
return err
}
pctx := pongo2.Context{
"request": request,
}
return wui.sendTemplate(ctx, "request-detail.html", pctx)
}

View file

@ -1,66 +0,0 @@
body {
line-height: 1.3;
margin: 0;
margin-bottom: 10px;
font-size: 18px;
font-family: sans-serif;
}
main {
padding: 15px;
}
div.pt {
padding-top: 15px;
}
.italic {
font-style: italic;
}
div.card-deck {
display: flex;
justify-content: space-between;
}
div.card-deck div.post {
flex: 1;
margin-right: 10px;
}
div.card-deck div.post:last-child {
margin-right: 0;
}
@media screen and (max-width: 800px) {
div.card-deck {
flex-direction: column;
}
div.card-deck div.card {
margin-right: unset;
margin-bottom: 10px;
}
div.card-deck div.card:last-child {
margin-bottom: 10px;
}
}
div.table table {
width: 60%;
white-space: nowrap;
border-collapse: collapse;
margin-bottom: 10px;
overflow-y: scroll;
}
div.table table tr:nth-child(even) {
background-color: #f2f2f2;
}
div.table table tr:hover {
background-color: #dddddd;
}
div.table table td, div.table table th {
padding: 4px 8px;
}
div.table table th {
border-bottom: 3px solid black;
background-color: unset;
}
/*# sourceMappingURL=main.css.map */

View file

@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["main.scss"],"names":[],"mappings":"AAAA;EACE;EAEA;EACA;EAEA;EACA;;;AAgBF;EAEE;;;AAKF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAIA;EACE;EACA,cAJY;;AAOd;EACE;;AAGF;EAfF;IAgBI;;EAEA;IAAW;IAAqB,eAdpB;;EAeZ;IAAsB,eAfV;;;;AAoBd;EACE;EACA;EACA;EACA;EAEA;;AAGE;EAAoB;;AACpB;EAAU;;AAGZ;EACE;;AAGF;EACE;EACA","file":"main.css"}

View file

@ -1,85 +0,0 @@
body {
line-height: 1.3;
margin: 0;
margin-bottom: 10px;
font-size: 18px;
font-family: sans-serif;
}
@mixin container {
width: 55%;
margin: 0 auto;
@media screen and (max-width: 800px) {
width: 98%;
}
@media screen and (max-width: 1100px) and (min-width: 801px) {
width: 70%;
}
}
main {
//@include container;
padding: 15px;
//padding-top: 15px;
//padding-bottom: 15px;
}
div.pt {
padding-top: 15px;
}
.italic {
font-style: italic;
}
div.card-deck {
display: flex;
justify-content: space-between;
$margin-size: 10px;
div.post {
flex: 1;
margin-right: $margin-size;
}
div.post:last-child {
margin-right: 0;
}
@media screen and (max-width: 800px) {
flex-direction: column;
div.card { margin-right: unset; margin-bottom: $margin-size; }
div.card:last-child { margin-bottom: $margin-size; }
}
}
div.table {
table {
width: 60%;
white-space: nowrap;
border-collapse: collapse;
margin-bottom: 10px;
overflow-y: scroll;
tr {
&:nth-child(even) { background-color: #f2f2f2; }
&:hover { background-color: #dddddd; }
}
td, th {
padding: 4px 8px;
}
th {
border-bottom: 3px solid black;
background-color: unset;
}
}
}

View file

@ -1,72 +0,0 @@
package webui
import (
"github.com/flosch/pongo2/v6"
"github.com/gofiber/fiber/v2"
"strings"
)
type HTMLTable struct {
Path string
Headers []*HTMLTableHeader
Data func(sortKey, sortDirection string) ([][]any, error)
DefaultSortKey string
DefaultSortDirection string
ShowNumberOfEntries bool
}
type HTMLTableHeader struct {
Name string
Slug string
Sortable bool
Safe bool
}
var unsetValue = pongo2.AsSafeValue(`<span class="italic">unset</span>`)
func (wui *WebUI) renderHTMLTable(ctx *fiber.Ctx, ht *HTMLTable) error {
sortKey := ctx.Query("sortKey")
sortDirection := strings.ToLower(ctx.Query("sortDir"))
{
var validatedSortKey string
for _, header := range ht.Headers {
if strings.EqualFold(header.Slug, sortKey) && header.Sortable {
validatedSortKey = header.Slug
break
}
}
sortKey = validatedSortKey
}
if sortKey == "" {
sortKey = ht.DefaultSortKey
}
if sortDirection == "" || !(sortDirection == "asc" || sortDirection == "desc") {
if sortKey == "" {
sortDirection = ""
} else {
sortDirection = ht.DefaultSortDirection
}
}
data, err := ht.Data(sortKey, sortDirection)
if err != nil {
return err
}
ctx.Type("html")
return wui.sendTemplate(ctx, "components/table.html", pongo2.Context{
"path": ht.Path,
"headers": ht.Headers,
"rows": data,
"sortKey": sortKey,
"sortDirection": sortDirection,
"showNumberOfEntries": ht.ShowNumberOfEntries,
})
}
func getValue(v *pongo2.Value, _ *pongo2.Error) *pongo2.Value {
return v
}

View file

@ -1,78 +0,0 @@
package webui
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/codemicro/analytics/analytics/db/models"
"github.com/flosch/pongo2/v6"
"github.com/gofiber/fiber/v2"
"html"
)
func (wui *WebUI) page_logsFromSession(ctx *fiber.Ctx) error {
id := ctx.Query("id")
if id == "" {
return fiber.ErrBadRequest
}
session := new(models.Session)
if err := wui.db.DB.NewSelect().Model(session).Where("id = ?", id).Scan(context.Background(), session); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fiber.ErrNotFound
}
return err
}
pctx := pongo2.Context{
"session": session,
}
return wui.sendTemplate(ctx, "logs-from-session.html", pctx)
}
func (wui *WebUI) partial_logsFromSession(ctx *fiber.Ctx) error {
id := ctx.Params("id")
pa := fmt.Sprintf("/partial/sessionLogs/%s", html.EscapeString(id))
ht := &HTMLTable{
Path: pa,
Headers: []*HTMLTableHeader{
{"Datetime", "time", true, false},
{"Host", "", false, false},
{"Raw path", "raw_uri", true, false},
{"Status", "status_code", true, false},
{"Referer", "", false, false},
{"", "", false, true},
},
Data: func(sortKey, sortDirection string) ([][]any, error) {
var reqs []*models.Request
q := wui.db.DB.NewSelect().Model(&reqs).Where("session_id = ?", id)
if sortKey != "" {
q = q.Order(sortKey + " " + sortDirection)
}
if err := q.Scan(context.Background(), &reqs); err != nil {
return nil, err
}
var res [][]any
for _, request := range reqs {
res = append(res, []any{
getValue(pongo2.ApplyFilter("date", pongo2.AsValue(request.Time), pongo2.AsValue("2006-01-02 15:04:05"))),
request.Host,
request.RawURI,
request.StatusCode,
getValue(pongo2.ApplyFilter("default", pongo2.AsValue(request.Referer), unsetValue)),
fmt.Sprintf(`<a href="/request?id=%s">[Link]</a>`, request.ID),
})
}
return res, nil
},
DefaultSortKey: "time",
DefaultSortDirection: "desc",
ShowNumberOfEntries: true,
}
return wui.renderHTMLTable(ctx, ht)
}

View file

@ -1,116 +0,0 @@
package webui
import (
"embed"
"fmt"
"github.com/codemicro/analytics/analytics/config"
"github.com/codemicro/analytics/analytics/db"
"github.com/codemicro/analytics/analytics/webui/internal/templates"
"github.com/flosch/pongo2/v6"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/rs/zerolog/log"
"net/http"
"time"
)
type WebUI struct {
conf *config.Config
db *db.DB
app *fiber.App
templates *pongo2.TemplateSet
}
func init() {
pongo2.RegisterFilter("shortTimeSince", func(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
tn := time.Now().UTC()
t := in.Time()
dur := tn.Sub(t).Round(time.Second)
var (
qty int
descriptor string
)
if int(dur.Minutes()) != 0 {
qty = int(dur.Minutes())
descriptor = "minute"
} else if int(dur.Seconds()) > 30 {
qty = int(dur.Seconds())
descriptor = "second"
} else {
return pongo2.AsValue("just now"), nil
}
if qty != 1 {
descriptor += "s"
}
return pongo2.AsValue(fmt.Sprintf("%d %s ago", qty, descriptor)), nil
})
}
func Start(conf *config.Config, db *db.DB) *WebUI {
wui := &WebUI{
conf: conf,
db: db,
}
wui.app = fiber.New(fiber.Config{
DisableStartupMessage: !config.Debug,
})
wui.registerHandlers()
wui.templates = pongo2.NewSet("templates", templates.TemplateLoader())
go func() {
if err := wui.app.Listen(conf.HTTP.Address); err != nil {
log.Error().Err(err).Msg("HTTP server listen failed")
return
}
}()
log.Info().Msgf("HTTP server alive on %s", conf.HTTP.Address)
return wui
}
func (wui *WebUI) sendTemplate(ctx *fiber.Ctx, fname string, renderCtx pongo2.Context) error {
tpl, err := wui.templates.FromFile(fname)
if err != nil {
return err
}
res, err := tpl.ExecuteBytes(renderCtx)
if err != nil {
return err
}
ctx.Type("html")
return ctx.Send(res)
}
func (wui *WebUI) Stop() error {
return wui.app.ShutdownWithTimeout(time.Second * 5)
}
//go:embed static/*
var static embed.FS
func (wui *WebUI) registerHandlers() {
wui.app.Get("/", wui.page_index)
wui.app.Get("/partial/activeSessions", wui.partial_activeSessionsTable)
wui.app.Get("/session", wui.page_logsFromSession)
wui.app.Get("/partial/sessionLogs/:id", wui.partial_logsFromSession)
wui.app.Get("/partial/topURLs", wui.partial_topURLs)
wui.app.Get("/request", wui.page_requestDetail)
wui.app.Get("/sessions", wui.page_listSessions)
wui.app.Get("/partial/listSessions", wui.partial_listSessions)
wui.app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(static),
PathPrefix: "static",
}))
}