Update
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
<div id="admin-controls" class="admin-controls hidden">
|
||||
<button type="button" id="reveal-btn" class="btn">Reveal</button>
|
||||
<button type="button" id="reset-btn" class="btn">Reset</button>
|
||||
<button type="button" id="terminal-btn" class="btn">Terminal</button>
|
||||
</div>
|
||||
<p id="room-status" class="status-line">Waiting for join...</p>
|
||||
</section>
|
||||
@@ -148,6 +149,20 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="terminal-modal-overlay" class="terminal-modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="terminal-title">
|
||||
<section class="window terminal-window">
|
||||
<div class="title-bar">
|
||||
<span id="terminal-title">RoomTerminal.exe</span>
|
||||
<div class="title-bar-controls">
|
||||
<button type="button" id="terminal-close-btn">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-content terminal-window-content">
|
||||
<div id="terminal-log-output" class="terminal-log-output" aria-live="polite"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="taskbar desktop-taskbar" aria-label="Desktop taskbar">
|
||||
|
||||
Reference in New Issue
Block a user