From 61417dbf924e87428a3a585f61660f096cf5c983 Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Thu, 5 Mar 2026 22:41:16 +0200 Subject: [PATCH] Update --- src/state/manager.go | 65 ++++++++++++++++++++++++++++++++++++----- src/state/types.go | 13 +++++++++ src/templates/room.html | 15 ++++++++++ static/css/layout.css | 26 +++++++++++++++++ static/css/main.css | 24 +++++++++++++++ static/js/room.js | 60 +++++++++++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 7 deletions(-) diff --git a/src/state/manager.go b/src/state/manager.go index 8728ebd..77efae0 100644 --- a/src/state/manager.go +++ b/src/state/manager.go @@ -2,11 +2,17 @@ package state import ( "encoding/json" + "fmt" "slices" "strings" "sync" ) +const ( + maxActivityLogEntries = 400 + adminLogBroadcastLimit = 200 +) + type Manager struct { mu sync.RWMutex rooms map[string]*Room @@ -111,8 +117,10 @@ func (m *Manager) CreateRoom(input CreateRoomInput) (CreateRoomResult, error) { Participants: map[string]*Participant{ creatorID: creator, }, + ActivityLog: []ActivityLogEntry{}, subscribers: map[string]*subscriber{}, } + m.appendActivityLogLocked(room, "%s created the room as admin.", creator.Username) m.mu.Lock() m.rooms[roomID] = room @@ -176,12 +184,16 @@ func (m *Manager) JoinRoom(roomID string, input JoinRoomInput) (JoinRoomResult, return JoinRoomResult{}, ErrParticipantNotFound } + wasConnected := existing.Connected existing.Username = username existing.Connected = true existing.UpdatedAt = now if isAdminByToken { existing.IsAdmin = true } + if !wasConnected { + m.appendActivityLogLocked(room, "%s joined as %s.", existing.Username, existing.Role) + } room.UpdatedAt = now if err := m.store.Save(room); err != nil { @@ -225,6 +237,7 @@ func (m *Manager) JoinRoom(roomID string, input JoinRoomInput) (JoinRoomResult, } room.Participants[participant.ID] = participant + m.appendActivityLogLocked(room, "%s joined as %s.", participant.Username, participant.Role) room.UpdatedAt = now if err := m.store.Save(room); err != nil { @@ -254,9 +267,11 @@ func (m *Manager) LeaveRoom(roomID, participantID string) error { return ErrParticipantNotFound } - participant.Connected = false - participant.UpdatedAt = nowUTC() - room.UpdatedAt = nowUTC() + if !participant.Connected { + return nil + } + m.disconnectParticipantLocked(room, participant) + m.appendActivityLogLocked(room, "%s left the room.", participant.Username) if err := m.store.Save(room); err != nil { return err @@ -300,9 +315,11 @@ func (m *Manager) CastVote(roomID, participantID, card string) error { participant.VoteValue = normalizedCard participant.UpdatedAt = nowUTC() room.UpdatedAt = nowUTC() + m.appendActivityLogLocked(room, "%s voted %s.", participant.Username, normalizedCard) if room.Settings.RevealMode == RevealModeAutoAll && allActiveParticipantsVoted(room) { room.Round.Revealed = true + m.appendActivityLogLocked(room, "Votes auto-revealed after all active participants voted.") } if err := m.store.Save(room); err != nil { @@ -332,6 +349,7 @@ func (m *Manager) RevealVotes(roomID, participantID string) error { room.Round.Revealed = true room.UpdatedAt = nowUTC() + m.appendActivityLogLocked(room, "%s revealed the votes.", participant.Username) if err := m.store.Save(room); err != nil { return err @@ -360,6 +378,7 @@ func (m *Manager) ResetVotes(roomID, participantID string) error { m.resetVotesLocked(room) room.UpdatedAt = nowUTC() + m.appendActivityLogLocked(room, "%s reset all votes.", participant.Username) if err := m.store.Save(room); err != nil { return err @@ -421,10 +440,11 @@ func (m *Manager) Subscribe(roomID, participantID string) (<-chan []byte, []byte delete(roomRef.subscribers, subscriberID) if p, participantOK := roomRef.Participants[participantID]; participantOK { - p.Connected = false - p.UpdatedAt = nowUTC() - roomRef.UpdatedAt = nowUTC() - _ = m.store.Save(roomRef) + if p.Connected { + m.disconnectParticipantLocked(roomRef, p) + m.appendActivityLogLocked(roomRef, "%s disconnected.", p.Username) + _ = m.store.Save(roomRef) + } } roomRef.mu.Unlock() @@ -461,6 +481,7 @@ func (m *Manager) loadFromDisk() error { Settings: persisted.Settings, Round: persisted.Round, Participants: make(map[string]*Participant, len(persisted.Participants)), + ActivityLog: append([]ActivityLogEntry(nil), persisted.ActivityLog...), subscribers: map[string]*subscriber{}, } for _, participant := range persisted.Participants { @@ -489,6 +510,7 @@ func (room *Room) toPersisted() persistedRoom { Settings: room.Settings, Round: room.Round, Participants: participants, + ActivityLog: append([]ActivityLogEntry(nil), room.ActivityLog...), } } @@ -572,6 +594,18 @@ func (m *Manager) marshalRoomState(room *Room, viewerParticipantID string) ([]by if viewer.IsAdmin { state.Links.AdminLink = "/room/" + room.ID + "?adminToken=" + room.AdminToken + + start := 0 + if len(room.ActivityLog) > adminLogBroadcastLimit { + start = len(room.ActivityLog) - adminLogBroadcastLimit + } + state.AdminLogs = make([]PublicActivityLogEntry, 0, len(room.ActivityLog)-start) + for _, item := range room.ActivityLog[start:] { + state.AdminLogs = append(state.AdminLogs, PublicActivityLogEntry{ + At: item.At, + Message: item.Message, + }) + } } return json.Marshal(state) @@ -606,3 +640,20 @@ func (m *Manager) broadcastRoom(roomID string) { } } } + +func (m *Manager) appendActivityLogLocked(room *Room, format string, args ...any) { + room.ActivityLog = append(room.ActivityLog, ActivityLogEntry{ + At: nowUTC(), + Message: fmt.Sprintf(format, args...), + }) + + if len(room.ActivityLog) > maxActivityLogEntries { + room.ActivityLog = append([]ActivityLogEntry(nil), room.ActivityLog[len(room.ActivityLog)-maxActivityLogEntries:]...) + } +} + +func (m *Manager) disconnectParticipantLocked(room *Room, participant *Participant) { + participant.Connected = false + participant.UpdatedAt = nowUTC() + room.UpdatedAt = nowUTC() +} diff --git a/src/state/types.go b/src/state/types.go index af7ce11..7514807 100644 --- a/src/state/types.go +++ b/src/state/types.go @@ -54,6 +54,11 @@ type RoundState struct { Revealed bool `json:"revealed"` } +type ActivityLogEntry struct { + At time.Time `json:"at"` + Message string `json:"message"` +} + type persistedRoom struct { ID string `json:"id"` AdminToken string `json:"adminToken"` @@ -62,6 +67,7 @@ type persistedRoom struct { Settings RoomSettings `json:"settings"` Round RoundState `json:"round"` Participants []*Participant `json:"participants"` + ActivityLog []ActivityLogEntry `json:"activityLog,omitempty"` } type subscriber struct { @@ -77,6 +83,7 @@ type Room struct { Settings RoomSettings Round RoundState Participants map[string]*Participant + ActivityLog []ActivityLogEntry mu sync.RWMutex subscribers map[string]*subscriber @@ -133,6 +140,11 @@ type RoomLinks struct { AdminLink string `json:"adminLink,omitempty"` } +type PublicActivityLogEntry struct { + At time.Time `json:"at"` + Message string `json:"message"` +} + type PublicRoomState struct { RoomID string `json:"roomId"` RoomName string `json:"roomName"` @@ -148,4 +160,5 @@ type PublicRoomState struct { SelfParticipantID string `json:"selfParticipantId"` ViewerIsAdmin bool `json:"viewerIsAdmin"` Links RoomLinks `json:"links"` + AdminLogs []PublicActivityLogEntry `json:"adminLogs,omitempty"` } diff --git a/src/templates/room.html b/src/templates/room.html index f00514c..f73808d 100644 --- a/src/templates/room.html +++ b/src/templates/room.html @@ -109,6 +109,7 @@

Waiting for join...

@@ -148,6 +149,20 @@ + +