diff --git a/.air.toml b/.air.toml
index ad5aa4c..8d71cf6 100644
--- a/.air.toml
+++ b/.air.toml
@@ -3,7 +3,7 @@ testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
- args_bin = ["--htmxlog"]
+ args_bin = ["--dev"]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/oslstats"
delay = 1000
@@ -14,7 +14,7 @@ tmp_dir = "tmp"
follow_symlink = false
full_bin = ""
include_dir = []
- include_ext = ["go", "templ"]
+ include_ext = ["go", "templ", "js"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
diff --git a/cmd/oslstats/httpserver.go b/cmd/oslstats/httpserver.go
index f13927b..b01c16e 100644
--- a/cmd/oslstats/httpserver.go
+++ b/cmd/oslstats/httpserver.go
@@ -35,6 +35,12 @@ func setupHttpServer(
ignoredPaths := []string{
"/static/css/output.css",
"/static/favicon.ico",
+ "/static/js/popups.js",
+ "/static/js/theme.js",
+ "/static/vendored/htmx@2.0.8.min.js",
+ "/static/vendored/htmx-ext-ws.min.js",
+ "/static/vendored/alpinejs@3.15.4.min.js",
+ "/ws/notifications",
}
auth, err := setupAuth(cfg.HWSAuth, logger, bun, httpServer, ignoredPaths)
@@ -62,7 +68,7 @@ func setupHttpServer(
return nil, errors.Wrap(err, "addRoutes")
}
- err = addMiddleware(httpServer, auth, cfg.Flags)
+ err = addMiddleware(httpServer, auth, cfg)
if err != nil {
return nil, errors.Wrap(err, "addMiddleware")
}
diff --git a/cmd/oslstats/middleware.go b/cmd/oslstats/middleware.go
index ff2c95c..8dd5cd8 100644
--- a/cmd/oslstats/middleware.go
+++ b/cmd/oslstats/middleware.go
@@ -3,6 +3,7 @@ package main
import (
"context"
"net/http"
+ "strconv"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
@@ -17,12 +18,12 @@ import (
func addMiddleware(
server *hws.Server,
auth *hwsauth.Authenticator[*db.User, bun.Tx],
- flags *config.Flags,
+ cfg *config.Config,
) error {
err := server.AddMiddleware(
auth.Authenticate(),
- htmxLog(flags.HTMXLog),
+ devMode(cfg),
)
if err != nil {
return errors.Wrap(err, "server.AddMiddleware")
@@ -30,12 +31,20 @@ func addMiddleware(
return nil
}
-func htmxLog(htmxlog bool) hws.Middleware {
+func devMode(cfg *config.Config) hws.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- ctx := context.WithValue(r.Context(), contexts.HTMXLogKey, htmxlog)
- req := r.WithContext(ctx)
- next.ServeHTTP(w, req)
+ if cfg.Flags.DevMode {
+ devInfo := contexts.DevInfo{
+ WebsocketBase: "ws://" + cfg.HWS.Host + ":" + strconv.FormatUint(cfg.HWS.Port, 10),
+ HTMXLog: true,
+ }
+ ctx := context.WithValue(r.Context(), contexts.DevModeKey, devInfo)
+ req := r.WithContext(ctx)
+ next.ServeHTTP(w, req)
+ return
+ }
+ next.ServeHTTP(w, r)
},
)
}
diff --git a/cmd/oslstats/routes.go b/cmd/oslstats/routes.go
index 9a577a9..28b4696 100644
--- a/cmd/oslstats/routes.go
+++ b/cmd/oslstats/routes.go
@@ -16,7 +16,7 @@ import (
)
func addRoutes(
- server *hws.Server,
+ s *hws.Server,
staticFS *http.FileSystem,
cfg *config.Config,
conn *bun.DB,
@@ -25,41 +25,42 @@ func addRoutes(
discordAPI *discord.APIClient,
) error {
// Create the routes
- routes := []hws.Route{
+ pageroutes := []hws.Route{
{
Path: "/static/",
Method: hws.MethodGET,
- Handler: http.StripPrefix("/static/", handlers.StaticFS(staticFS, server)),
+ Handler: http.StripPrefix("/static/", handlers.StaticFS(staticFS, s)),
},
{
Path: "/",
Method: hws.MethodGET,
- Handler: handlers.Index(server),
+ Handler: handlers.Index(s),
},
{
Path: "/login",
Method: hws.MethodGET,
- Handler: auth.LogoutReq(handlers.Login(server, cfg, store, discordAPI)),
+ Handler: auth.LogoutReq(handlers.Login(s, cfg, store, discordAPI)),
},
{
Path: "/auth/callback",
Method: hws.MethodGET,
- Handler: auth.LogoutReq(handlers.Callback(server, auth, conn, cfg, store, discordAPI)),
+ Handler: auth.LogoutReq(handlers.Callback(s, auth, conn, cfg, store, discordAPI)),
},
{
Path: "/register",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
- Handler: auth.LogoutReq(handlers.Register(server, auth, conn, cfg, store)),
+ Handler: auth.LogoutReq(handlers.Register(s, auth, conn, cfg, store)),
},
{
Path: "/logout",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
- Handler: auth.LoginReq(handlers.Logout(server, auth, conn, discordAPI)),
+ Handler: auth.LoginReq(handlers.Logout(s, auth, conn, discordAPI)),
},
{
- Path: "/test",
+ Path: "/notification-tester",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
- Handler: handlers.Test(server),
+ Handler: handlers.NotifyTester(s),
+ // TODO: add login protection
},
}
@@ -67,12 +68,23 @@ func addRoutes(
{
Path: "/htmx/isusernameunique",
Method: hws.MethodPOST,
- Handler: handlers.IsUsernameUnique(server, conn, cfg, store),
+ Handler: handlers.IsUsernameUnique(s, conn, cfg, store),
},
}
+ wsRoutes := []hws.Route{
+ {
+ Path: "/ws/notifications",
+ Method: hws.MethodGET,
+ Handler: handlers.NotificationWS(s, cfg),
+ },
+ }
+
+ routes := append(pageroutes, htmxRoutes...)
+ routes = append(routes, wsRoutes...)
+
// Register the routes with the server
- err := server.AddRoutes(append(routes, htmxRoutes...)...)
+ err := s.AddRoutes(routes...)
if err != nil {
return errors.Wrap(err, "server.AddRoutes")
}
diff --git a/cmd/oslstats/run.go b/cmd/oslstats/run.go
index 2d6f34f..83fc280 100644
--- a/cmd/oslstats/run.go
+++ b/cmd/oslstats/run.go
@@ -73,8 +73,9 @@ func run(ctx context.Context, w io.Writer, cfg *config.Config) error {
wg.Go(func() {
<-ctx.Done()
shutdownCtx := context.Background()
- shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
+ shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 60*time.Second)
defer cancel()
+ logger.Info().Msg("Shut down requested, waiting 60 seconds...")
err := httpServer.Shutdown(shutdownCtx)
if err != nil {
logger.Error().Err(err).Msg("Graceful shutdown failed")
diff --git a/go.mod b/go.mod
index efde9d7..6e20442 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,11 @@ require (
git.haelnorr.com/h/golib/env v0.9.1
git.haelnorr.com/h/golib/ezconf v0.1.1
git.haelnorr.com/h/golib/hlog v0.10.4
- git.haelnorr.com/h/golib/hws v0.3.2
+ git.haelnorr.com/h/golib/hws v0.4.0
git.haelnorr.com/h/golib/hwsauth v0.5.2
+ git.haelnorr.com/h/golib/notify v0.1.0
github.com/a-h/templ v0.3.977
+ github.com/coder/websocket v1.8.14
github.com/joho/godotenv v1.5.1
github.com/pkg/errors v0.9.1
github.com/uptrace/bun v1.2.16
@@ -18,7 +20,7 @@ require (
require (
github.com/gorilla/websocket v1.4.2 // indirect
- golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/crypto v0.47.0 // indirect
)
require (
diff --git a/go.sum b/go.sum
index f226f23..cb5707d 100644
--- a/go.sum
+++ b/go.sum
@@ -6,18 +6,22 @@ git.haelnorr.com/h/golib/ezconf v0.1.1 h1:4euTSDb9jvuQQkVq+x5gHoYPYyUZPWxoOSlWCI
git.haelnorr.com/h/golib/ezconf v0.1.1/go.mod h1:rETDcjpcEyyeBgCiZSU617wc0XycwZSC5+IAOtXmwP8=
git.haelnorr.com/h/golib/hlog v0.10.4 h1:vpCsV/OddjIYx8F48U66WxojjmhEbeLGQAOBG4ViSRQ=
git.haelnorr.com/h/golib/hlog v0.10.4/go.mod h1:+wJ8vecQY/JITTXKmI3JfkHiUGyMs7N6wooj2wuWZbc=
-git.haelnorr.com/h/golib/hws v0.3.2 h1:OSwCwVxDerermyuoxrAYgt+i1zzwF/0Baoy8zmCxtuQ=
-git.haelnorr.com/h/golib/hws v0.3.2/go.mod h1:6ZlRKnt8YMpv5XcMXmyBGmD1/euvBo3d1azEvHJjOLo=
+git.haelnorr.com/h/golib/hws v0.4.0 h1:T2JfRz4zpgsNXj0Vyfzxdf/60Tee/7H30osFmr5jDh0=
+git.haelnorr.com/h/golib/hws v0.4.0/go.mod h1:UqB83p9lGjidDkk0pWRqxxOFrCkg8t+9J6uGtBOjNLo=
git.haelnorr.com/h/golib/hwsauth v0.5.2 h1:K4McXMEHtI5o4fAL3AZrmaMkwORNqSTV3MM6BExNKag=
git.haelnorr.com/h/golib/hwsauth v0.5.2/go.mod h1:NOonrVU/lX8lzuV77eDEiTwBjn7RrzYVcSdXUJWeHmQ=
git.haelnorr.com/h/golib/jwt v0.10.1 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI=
git.haelnorr.com/h/golib/jwt v0.10.1/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
+git.haelnorr.com/h/golib/notify v0.1.0 h1:xdf6zd21F6n+SuGTeJiuLNMf6zFXMvwpKD0gmNq8N10=
+git.haelnorr.com/h/golib/notify v0.1.0/go.mod h1:ARqaRmCYb8LMURhDM75sG+qX+YpqXmUVeAtacwjHjBc=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
+github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
+github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -71,8 +75,8 @@ go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwEx
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/internal/config/flags.go b/internal/config/flags.go
index ff42621..27dfd91 100644
--- a/internal/config/flags.go
+++ b/internal/config/flags.go
@@ -12,7 +12,7 @@ type Flags struct {
ShowEnv bool
GenEnv string
EnvFile string
- HTMXLog bool
+ DevMode bool
// Database reset (destructive)
ResetDB bool
@@ -34,7 +34,7 @@ func SetupFlags() (*Flags, error) {
showEnv := flag.Bool("showenv", false, "Print all environment variable values and their documentation")
genEnv := flag.String("genenv", "", "Generate a .env file with all environment variables (specify filename)")
envfile := flag.String("envfile", ".env", "Specify a .env file to use for the configuration")
- htmxlog := flag.Bool("htmxlog", false, "Run the server with all HTMX events logged")
+ devMode := flag.Bool("dev", false, "Run the server in dev mode")
// Database reset (destructive)
resetDB := flag.Bool("reset-db", false, "⚠️ DESTRUCTIVE: Drop and recreate all tables (dev only)")
@@ -78,7 +78,7 @@ func SetupFlags() (*Flags, error) {
ShowEnv: *showEnv,
GenEnv: *genEnv,
EnvFile: *envfile,
- HTMXLog: *htmxlog,
+ DevMode: *devMode,
ResetDB: *resetDB,
MigrateUp: *migrateUp,
MigrateRollback: *migrateRollback,
diff --git a/internal/handlers/notifswebsocket.go b/internal/handlers/notifswebsocket.go
new file mode 100644
index 0000000..694fde0
--- /dev/null
+++ b/internal/handlers/notifswebsocket.go
@@ -0,0 +1,120 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+
+ "git.haelnorr.com/h/golib/cookies"
+ "git.haelnorr.com/h/golib/hws"
+ "git.haelnorr.com/h/golib/notify"
+ "git.haelnorr.com/h/oslstats/internal/config"
+ "git.haelnorr.com/h/oslstats/internal/db"
+ "git.haelnorr.com/h/oslstats/internal/view/component/popup"
+
+ "github.com/coder/websocket"
+ "github.com/pkg/errors"
+)
+
+func NotificationWS(
+ s *hws.Server,
+ cfg *config.Config,
+) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("Upgrade") != "websocket" {
+ throwNotFound(s, w, r, r.URL.Path)
+ return
+ }
+ nc, err := setupClient(s, w, r)
+ if err != nil {
+ s.LogError(hws.HWSError{
+ Message: "Failed to get notification client",
+ Error: err,
+ Level: hws.ErrorERROR,
+ StatusCode: http.StatusInternalServerError,
+ })
+ return
+ }
+ ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
+ OriginPatterns: []string{cfg.HWSAuth.TrustedHost},
+ })
+ if err != nil {
+ s.LogError(hws.HWSError{
+ Message: "Failed to open websocket",
+ Error: err,
+ Level: hws.ErrorERROR,
+ StatusCode: http.StatusInternalServerError,
+ })
+ return
+ }
+ defer ws.CloseNow()
+ ctx := ws.CloseRead(r.Context())
+ err = notifyLoop(ctx, nc, ws)
+ if err != nil {
+ s.LogError(hws.HWSError{
+ Message: "Notification error",
+ Error: err,
+ Level: hws.ErrorERROR,
+ StatusCode: http.StatusInternalServerError,
+ })
+ }
+ },
+ )
+}
+
+func setupClient(s *hws.Server, w http.ResponseWriter, r *http.Request) (*hws.Client, error) {
+ user := db.CurrentUser(r.Context())
+ altID := ""
+ if user != nil {
+ altID = strconv.Itoa(user.ID)
+ }
+ subCookie, err := r.Cookie("ws_sub_id")
+ subID := ""
+ if err == nil {
+ subID = subCookie.Value
+ }
+ nc, err := s.GetClient(subID, altID)
+ if err != nil {
+ return nil, errors.Wrap(err, "s.GetClient")
+ }
+ cookies.SetCookie(w, "ws_sub_id", "/", nc.ID(), 0)
+ return nc, nil
+}
+
+func notifyLoop(ctx context.Context, c *hws.Client, ws *websocket.Conn) error {
+ notifs, stop := c.Listen()
+ defer close(stop)
+ count := 0
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+ case nt, ok := <-notifs:
+ count++
+ if !ok {
+ return nil
+ }
+ w, err := ws.Writer(ctx, websocket.MessageText)
+ if err != nil {
+ return errors.Wrap(err, "ws.Writer")
+ }
+ switch nt.Level {
+ case hws.LevelShutdown:
+ err = popup.Toast(nt, count, 30000).Render(ctx, w)
+ case notify.LevelWarn:
+ err = popup.Toast(nt, count, 10000).Render(ctx, w)
+ case notify.LevelError:
+ // do error modal
+ default:
+ err = popup.Toast(nt, count, 6000).Render(ctx, w)
+ }
+ if err != nil {
+ return errors.Wrap(err, "popup.Toast")
+ }
+ err = w.Close()
+ if err != nil {
+ return errors.Wrap(err, "w.Close")
+ }
+ }
+ }
+}
diff --git a/internal/handlers/test.go b/internal/handlers/test.go
index efa3eb1..f9091e9 100644
--- a/internal/handlers/test.go
+++ b/internal/handlers/test.go
@@ -7,36 +7,45 @@ import (
"github.com/pkg/errors"
"git.haelnorr.com/h/golib/hws"
+ "git.haelnorr.com/h/golib/notify"
)
// Handles responses to the / path. Also serves a 404 Page for paths that
// don't have explicit handlers
-func Test(server *hws.Server) http.Handler {
+func NotifyTester(server *hws.Server) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
page.Test().Render(r.Context(), w)
} else {
r.ParseForm()
- notifytype := r.Form.Get("type")
+ target := r.Form.Get("target")
title := r.Form.Get("title")
+ level := map[string]notify.Level{
+ "info": notify.LevelInfo,
+ "success": notify.LevelSuccess,
+ "warn": notify.LevelWarn,
+ "error": notify.LevelError,
+ }[r.Form.Get("type")]
+ error := errors.New("This is a stack trace. No really i swear. Just pretend ok? Thanks")
message := r.Form.Get("message")
- switch notifytype {
- case "error":
- err := errors.New(message)
- notifyInternalServiceError(server, w, title, err)
- return
- case "warn":
- notifyWarning(w, title, message)
- return
- case "info":
- notifyInfo(w, title, message)
- return
- case "success":
- notifySuccess(w, title, message)
- return
+ nt := notify.Notification{
+ Target: notify.Target(target),
+ Title: title,
+ Message: message,
+ Level: level,
+ Details: error.Error(),
+ }
+ if target == "" {
+ server.NotifyAll(nt)
+ } else {
+ server.NotifySub(nt)
}
}
},
)
}
+
+func notifyInfoWS() {
+
+}
diff --git a/internal/view/component/popup/toast.templ b/internal/view/component/popup/toast.templ
new file mode 100644
index 0000000..236f47c
--- /dev/null
+++ b/internal/view/component/popup/toast.templ
@@ -0,0 +1,110 @@
+package popup
+
+import "fmt"
+import "git.haelnorr.com/h/golib/notify"
+import "git.haelnorr.com/h/golib/hws"
+
+// ToastNotificationWS creates a toast notification sent via WebSocket with OOB swap
+// Backend should pass: notifType ("success"|"warning"|"info"), title, message
+templ Toast(nt notify.Notification, id, duration int) {
+ {{
+ // Determine classes server-side based on notifType
+ color := map[notify.Level]string{
+ notify.LevelSuccess: "green",
+ notify.LevelWarn: "yellow",
+ notify.LevelInfo: "blue",
+ hws.LevelShutdown: "yellow",
+ }
+ containerClass := fmt.Sprintf("bg-dark-%s border-2 border-%s", color[nt.Level], color[nt.Level])
+ textClass := fmt.Sprintf("text-%s", color[nt.Level])
+ progressClass := fmt.Sprintf("bg-%s", color[nt.Level])
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { nt.Title }
+
+
+ { nt.Message }
+
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/internal/view/component/popup/toastContainer.templ b/internal/view/component/popup/toastContainer.templ
index 4385dc3..9c660a1 100644
--- a/internal/view/component/popup/toastContainer.templ
+++ b/internal/view/component/popup/toastContainer.templ
@@ -1,10 +1,9 @@
package popup
// ToastContainer displays stacked toast notifications (success, warning, info)
+// Supports both regular toasts (AlpineJS) and WebSocket toasts (HTMX OOB swaps)
templ ToastContainer() {
-
- @ToastNotification()
-
+
}
diff --git a/internal/view/component/popup/toastNotification.templ b/internal/view/component/popup/toastNotification.templ
deleted file mode 100644
index bcdcc14..0000000
--- a/internal/view/component/popup/toastNotification.templ
+++ /dev/null
@@ -1,114 +0,0 @@
-package popup
-
-// ToastNotification displays an individual toast notification
-templ ToastNotification() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-}
diff --git a/internal/view/layout/global.templ b/internal/view/layout/global.templ
index 6a4d774..f428b51 100644
--- a/internal/view/layout/global.templ
+++ b/internal/view/layout/global.templ
@@ -8,13 +8,11 @@ import "git.haelnorr.com/h/oslstats/pkg/contexts"
// Global page layout. Includes HTML document settings, header tags
// navbar and footer
templ Global(title string) {
- {{ htmxLogging := contexts.HTMXLog(ctx) }}
+ {{ devInfo := contexts.DevMode(ctx) }}
{ title }
-
-
-
- if htmxLogging {
+
+
+
+
+ if devInfo.HTMXLog {
}
-
-
- @popup.ErrorModal()
- @popup.ToastContainer()
+
+
+ @popup.ToastContainer()
-
-
-
+
+
+
+
+
+
+
+