fix: address PR review findings (issues #3-#8)

- Fix double-close panic in Hub.Stop/GracefulStop using sync.Once (#3)
- Bump golangci-lint action to v9 with v2.11.3 for Go 1.25 support (#4)
- Add input validation guards to SearchMessages (#5)
- Handle promise rejections in InviteManager with error toasts (#6)
- Add missing reply_to and edited_at columns to admin test schema (#7)
- Add ClientCount to HubBroadcaster interface and wire into stats endpoint (#8)
This commit is contained in:
jevb
2026-03-17 03:20:37 +01:00
parent 8f4349ba42
commit 1b596367c4
9 changed files with 35 additions and 7 deletions
+2 -1
View File
@@ -40,8 +40,9 @@ jobs:
retention-days: 7
- name: Lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: v2.11.3
working-directory: Server/
client-check:
@@ -25,6 +25,7 @@ export interface InviteManagerOptions {
onRevokeInvite(code: string): Promise<void>;
onCopyLink(code: string): void;
onClose(): void;
onError?(message: string): void;
}
// ---------------------------------------------------------------------------
@@ -82,6 +83,8 @@ export function createInviteManager(
void options.onRevokeInvite(invite.code).then(() => {
invites = invites.filter((i) => i.code !== invite.code);
renderList();
}).catch(() => {
options.onError?.("Failed to revoke invite");
});
}, { signal: ac.signal });
@@ -114,6 +117,8 @@ export function createInviteManager(
void options.onCreateInvite().then((newInvite) => {
invites = [...invites, newInvite];
renderList();
}).catch(() => {
options.onError?.("Failed to create invite");
});
}, { signal: ac.signal });
@@ -159,6 +159,10 @@ export function createInviteManagerController(opts: {
void navigator.clipboard.writeText(code);
},
onClose: close,
onError: (message: string) => {
log.error(message);
opts.getToast()?.show(message, "error");
},
});
if (root !== null) {
instance.mount(root);
+2 -1
View File
@@ -49,6 +49,7 @@ type HubBroadcaster interface {
BroadcastChannelDelete(channelID int64)
BroadcastMemberBan(userID int64)
BroadcastMemberUpdate(userID int64, roleName string)
ClientCount() int
}
// ─── adminUserResponse ──────────────────────────────────────────────────────
@@ -124,7 +125,7 @@ func NewAdminAPI(database *db.DB, version string, hub HubBroadcaster, u *updater
r.Group(func(r chi.Router) {
r.Use(adminAuthMiddleware(database))
r.Get("/stats", handleGetStats(database))
r.Get("/stats", handleGetStats(database, hub))
r.Get("/users", handleListUsers(database))
r.Patch("/users/{id}", handlePatchUser(database, hub))
r.Delete("/users/{id}/sessions", handleForceLogout(database))
+8 -1
View File
@@ -81,7 +81,9 @@ CREATE TABLE IF NOT EXISTS messages (
content TEXT NOT NULL,
deleted INTEGER NOT NULL DEFAULT 0,
pinned INTEGER NOT NULL DEFAULT 0,
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
reply_to INTEGER REFERENCES messages(id) ON DELETE SET NULL,
edited_at TEXT
);
CREATE TABLE IF NOT EXISTS invites (
@@ -972,6 +974,7 @@ type mockHub struct {
channelDeleteIDs []int64
memberBanIDs []int64
memberUpdates []memberUpdateCall
clientCount int
}
type memberUpdateCall struct {
@@ -1008,6 +1011,10 @@ func (m *mockHub) BroadcastMemberUpdate(userID int64, roleName string) {
m.memberUpdates = append(m.memberUpdates, memberUpdateCall{userID, roleName})
}
func (m *mockHub) ClientCount() int {
return m.clientCount
}
func TestAdminAPI_CreateChannel_BroadcastsChannelCreate(t *testing.T) {
database := openAdminTestDB(t)
hub := &mockHub{}
+2 -1
View File
@@ -11,13 +11,14 @@ import (
// ─── User Handlers ───────────────────────────────────────────────────────────
func handleGetStats(database *db.DB) http.HandlerFunc {
func handleGetStats(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
stats, err := database.GetServerStats()
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to get stats")
return
}
stats.OnlineCount = hub.ClientCount()
writeJSON(w, http.StatusOK, stats)
}
}
+7
View File
@@ -186,6 +186,13 @@ func (d *DB) GetReactions(messageID int64) ([]ReactionCount, error) {
// When channelID is non-nil the search is scoped to that channel.
// Deleted messages are excluded from results.
func (d *DB) SearchMessages(query string, channelID *int64, limit int) ([]MessageSearchResult, error) {
if query == "" {
return []MessageSearchResult{}, nil
}
if limit < 1 {
return []MessageSearchResult{}, nil
}
var (
rows *sql.Rows
err error
+1
View File
@@ -170,6 +170,7 @@ type ServerStats struct {
ChannelCount int64 `json:"channel_count"`
InviteCount int64 `json:"invite_count"`
DBSizeBytes int64 `json:"db_size_bytes"`
OnlineCount int `json:"online_count"`
}
// UserWithRole extends User with the name of the user's role.
+4 -3
View File
@@ -26,6 +26,7 @@ type Hub struct {
register chan *Client
unregister chan *Client
stop chan struct{}
stopOnce sync.Once
sfu *SFU
voiceRooms map[int64]*VoiceRoom
voiceRoomsMu sync.RWMutex
@@ -149,9 +150,9 @@ func (h *Hub) Run() {
}
}
// Stop signals Run to exit.
// Stop signals Run to exit. Safe to call multiple times.
func (h *Hub) Stop() {
close(h.stop)
h.stopOnce.Do(func() { close(h.stop) })
}
// GracefulStop closes all PeerConnections, voice rooms, and then stops the hub.
@@ -166,7 +167,7 @@ func (h *Hub) GracefulStop() {
h.mu.RUnlock()
h.CloseAllVoiceRooms()
close(h.stop)
h.stopOnce.Do(func() { close(h.stop) })
}
// CleanupVoiceForChannel removes the voice room for the given channel and