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)
}