From 06b3839c3aae73afb0b4a5fbc5714a5cb8f2b413 Mon Sep 17 00:00:00 2001
From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com>
Date: Tue, 3 Mar 2026 10:25:04 -1000
Subject: [PATCH] refactor: patch connection indicator with timestamp
Server patches the ConnectionIndicator element with a timestamp on
each heartbeat. Client-side JS checks every second if the timestamp
is stale (>20s) and toggles red/green accordingly.
This properly detects connection loss since the indicator will turn
red if no patches are received.
---
features/c4game/handlers.go | 3 +-
features/c4game/pages/game.templ | 4 +-
features/common/components/shared.templ | 59 +++++++++++++++++++++----
features/snakegame/handlers.go | 3 +-
features/snakegame/pages/game.templ | 4 +-
5 files changed, 58 insertions(+), 15 deletions(-)
diff --git a/features/c4game/handlers.go b/features/c4game/handlers.go
index 8c37d01..7b153d9 100644
--- a/features/c4game/handlers.go
+++ b/features/c4game/handlers.go
@@ -16,6 +16,7 @@ import (
"github.com/ryanhamamura/games/connect4"
"github.com/ryanhamamura/games/db/repository"
"github.com/ryanhamamura/games/features/c4game/pages"
+ sharedcomponents "github.com/ryanhamamura/games/features/common/components"
"github.com/ryanhamamura/games/sessions"
)
@@ -119,7 +120,7 @@ func HandleGameEvents(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManag
}
sendPing := func() error {
- return sse.PatchSignals([]byte(fmt.Sprintf(`{"lastPing":%d}`, time.Now().UnixMilli())))
+ return sse.PatchElementTempl(sharedcomponents.ConnectionIndicator(time.Now().UnixMilli()))
}
// Send initial render and ping
diff --git a/features/c4game/pages/game.templ b/features/c4game/pages/game.templ
index 16204cf..455c91d 100644
--- a/features/c4game/pages/game.templ
+++ b/features/c4game/pages/game.templ
@@ -15,10 +15,10 @@ templ GamePage(g *connect4.Game, myColor int, messages []chat.Message, chatCfg c
@layouts.Base("Connect 4") {
- @sharedcomponents.ConnectionIndicator()
+ @sharedcomponents.ConnectionIndicator(0)
@GameContent(g, myColor, messages, chatCfg)
}
diff --git a/features/common/components/shared.templ b/features/common/components/shared.templ
index 76d67b6..a9b783b 100644
--- a/features/common/components/shared.templ
+++ b/features/common/components/shared.templ
@@ -1,6 +1,10 @@
package components
-import "github.com/starfederation/datastar-go/datastar"
+import (
+ "fmt"
+
+ "github.com/starfederation/datastar-go/datastar"
+)
templ BackToLobby() {
← Back
@@ -44,19 +48,56 @@ templ NicknamePrompt(returnPath string) {
}
+func isStale(lastPing int64) bool {
+ return lastPing == 0
+}
+
// ConnectionIndicator shows a small dot indicating SSE connection status.
-// It requires a `lastPing` signal (unix ms timestamp) to be set by the server.
-templ ConnectionIndicator() {
+// Server patches this with a timestamp; client JS detects staleness.
+templ ConnectionIndicator(lastPing int64) {
+ @connectionWatcher()
+}
+
+script connectionWatcher() {
+ setInterval(function() {
+ var el = document.getElementById('connection-indicator');
+ var dot = document.getElementById('connection-dot');
+ var ping = document.getElementById('connection-ping');
+ if (!el || !dot || !ping) return;
+
+ var lastPing = parseInt(el.dataset.lastPing, 10) || 0;
+ var stale = Date.now() - lastPing > 20000;
+
+ dot.classList.toggle('status-success', !stale);
+ dot.classList.toggle('status-error', stale);
+ ping.classList.toggle('status-success', !stale);
+ ping.classList.toggle('status-error', stale);
+ ping.classList.toggle('animate-ping', !stale);
+ }, 1000);
}
templ GameJoinPrompt(loginURL string, registerURL string, gamePath string) {
diff --git a/features/snakegame/handlers.go b/features/snakegame/handlers.go
index d5aac7b..4448beb 100644
--- a/features/snakegame/handlers.go
+++ b/features/snakegame/handlers.go
@@ -13,6 +13,7 @@ import (
"github.com/ryanhamamura/games/chat"
chatcomponents "github.com/ryanhamamura/games/chat/components"
+ sharedcomponents "github.com/ryanhamamura/games/features/common/components"
"github.com/ryanhamamura/games/features/snakegame/pages"
"github.com/ryanhamamura/games/sessions"
"github.com/ryanhamamura/games/snake"
@@ -125,7 +126,7 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
}
sendPing := func() error {
- return sse.PatchSignals([]byte(fmt.Sprintf(`{"lastPing":%d}`, time.Now().UnixMilli())))
+ return sse.PatchElementTempl(sharedcomponents.ConnectionIndicator(time.Now().UnixMilli()))
}
// Send initial render and ping
diff --git a/features/snakegame/pages/game.templ b/features/snakegame/pages/game.templ
index f8fe244..a1b2cd4 100644
--- a/features/snakegame/pages/game.templ
+++ b/features/snakegame/pages/game.templ
@@ -32,12 +32,12 @@ templ GamePage(sg *snake.SnakeGame, mySlot int, messages []chat.Message, chatCfg
@layouts.Base("Snake") {
- @components.ConnectionIndicator()
+ @components.ConnectionIndicator(0)
@GameContent(sg, mySlot, messages, chatCfg, gameID)
}