diff --git a/internal/store/store.go b/internal/store/store.go index ebdc845..516b0e3 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -231,6 +231,14 @@ func escapeLike(s string) string { return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s) } +// nameOr is a record's player_name, or when that's empty (SimpleAdmin's offline css_addban and +// css_addgag etc. save none) the player's latest name from sa_players_ips. +func nameOr(alias string) string { + return `COALESCE(NULLIF(` + alias + `.player_name,''), (SELECT i.name FROM sa_players_ips i + WHERE i.steamid = ` + alias + `.player_steamid AND i.name IS NOT NULL AND i.name <> '' + ORDER BY i.used_at DESC LIMIT 1), '')` +} + // Bans lists sa_bans, newest first, with the total matching count. func (s *Store) Bans(ctx context.Context, q Query) ([]Penalty, int, error) { where, args := s.where("sa_bans", q) @@ -238,7 +246,7 @@ func (s *Store) Bans(ctx context.Context, q Query) ([]Penalty, int, error) { if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sa_bans"+where, args...).Scan(&total); err != nil { return nil, 0, err } - sqlq := `SELECT b.id, COALESCE(b.player_name,''), b.player_steamid, COALESCE(b.player_ip,''), b.admin_steamid, + sqlq := `SELECT b.id, ` + nameOr("b") + `, b.player_steamid, COALESCE(b.player_ip,''), b.admin_steamid, b.admin_name, b.reason, b.duration, b.ends, b.created, b.status, COALESCE(ua.player_name,''), COALESCE(u.reason,'') FROM (SELECT * FROM sa_bans` + where + ` ORDER BY id DESC LIMIT ? OFFSET ?) b @@ -275,7 +283,7 @@ func (s *Store) Comms(ctx context.Context, q Query) ([]Penalty, int, error) { if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sa_mutes"+where, args...).Scan(&total); err != nil { return nil, 0, err } - sqlq := `SELECT m.id, m.type, COALESCE(m.player_name,''), m.player_steamid, m.admin_steamid, m.admin_name, + sqlq := `SELECT m.id, m.type, ` + nameOr("m") + `, m.player_steamid, m.admin_steamid, m.admin_name, m.reason, m.duration, m.passed, m.ends, m.created, m.status, COALESCE(ua.player_name,''), COALESCE(u.reason,'') FROM (SELECT * FROM sa_mutes` + where + ` ORDER BY id DESC LIMIT ? OFFSET ?) m @@ -873,7 +881,7 @@ func (s *Store) MaxID(ctx context.Context, table string) int64 { // ClaimConsoleRow finds a row created after afterID for the player by "Console" (admin_steamid 0) // and credits it to the web admin instead. SimpleAdmin writes the row asynchronously, so this polls // for a few seconds. It returns the row id, or 0 if none appeared. -func (s *Store) ClaimConsoleRow(ctx context.Context, table string, afterID int64, steamid, commType, adminSteamID, adminName string) int64 { +func (s *Store) ClaimConsoleRow(ctx context.Context, table string, afterID int64, steamid, commType, adminSteamID, adminName, playerName string) int64 { n, _ := parseSID(steamid) a, _ := parseSID(adminSteamID) q := `SELECT id FROM ` + table + ` WHERE id > ? AND player_steamid = ? AND admin_steamid = 0` @@ -886,7 +894,9 @@ func (s *Store) ClaimConsoleRow(ctx context.Context, table string, afterID int64 for range 20 { var id int64 if err := s.db.QueryRowContext(ctx, q, args...).Scan(&id); err == nil { - _, _ = s.db.ExecContext(ctx, `UPDATE `+table+` SET admin_steamid = ?, admin_name = ? WHERE id = ?`, a, adminName, id) + // Offline penalties arrive without a player name, so fill it in from what we know. + _, _ = s.db.ExecContext(ctx, `UPDATE `+table+` SET admin_steamid = ?, admin_name = ?, + player_name = COALESCE(NULLIF(player_name, ''), NULLIF(?, '')) WHERE id = ?`, a, adminName, playerName, id) return id } select { diff --git a/internal/web/actions.go b/internal/web/actions.go index 10d2327..6fd726a 100644 --- a/internal/web/actions.go +++ b/internal/web/actions.go @@ -183,7 +183,7 @@ func (s *Server) addPenalty(w http.ResponseWriter, r *http.Request, id *Identity if req.Kind == "comm" { commType = req.Type } - claimed := s.st.ClaimConsoleRow(ctx, table, before, req.SteamID, commType, id.SteamID, id.Name) + claimed := s.st.ClaimConsoleRow(ctx, table, before, req.SteamID, commType, id.SteamID, id.Name, s.st.LatestName(ctx, req.SteamID)) go s.poll.Refresh() slog.Info("penalty issued", "admin", id.SteamID, "kind", req.Kind, "type", req.Type, "player", req.SteamID, "minutes", req.Duration) if claimed == 0 { diff --git a/internal/web/admin.go b/internal/web/admin.go index e2d1de2..d41fb87 100644 --- a/internal/web/admin.go +++ b/internal/web/admin.go @@ -281,6 +281,7 @@ var knownFlags = [][3]string{ {"@css/config", "Config", "Exec config files"}, {"@css/rcon", "RCON", "Run raw console commands"}, {"@css/cheats", "Cheats", "sv_cheats-protected commands"}, + {"@css/bypassipban", "Bypass IP bans", "Joins even when their IP, or another account on it, is banned"}, {"@css/vip", "VIP", "VIP perks in other plugins"}, {"@css/root", "Root", "Every permission, and managing ranks and staff"}, } diff --git a/internal/web/assets/admin.html b/internal/web/assets/admin.html index bd8201b..af7cca7 100644 --- a/internal/web/assets/admin.html +++ b/internal/web/assets/admin.html @@ -12,6 +12,7 @@
+
diff --git a/internal/web/assets/admin.js b/internal/web/assets/admin.js index a9baff5..6befcc7 100644 --- a/internal/web/assets/admin.js +++ b/internal/web/assets/admin.js @@ -648,12 +648,18 @@ function renderNav() { $("#nav-links").innerHTML = PAGES.map((p) => `
  • ${p.label}${count[p.id] !== undefined && count[p.id] !== "" ? `${esc(count[p.id])}` : ""}
  • `).join(""); } +// The mobile sidebar covers the Menu button, so it closes from its own Close button, the +// backdrop, Escape, or picking a page. +function setNav(open) { + $("#nav").classList.toggle("open", open); + $("#menu-btn").setAttribute("aria-expanded", String(open)); +} + function route() { const page = location.hash.slice(1); state.page = RENDER[page] ? page : "overview"; renderNav(); - $("#nav").classList.remove("open"); - $("#menu-btn").setAttribute("aria-expanded", "false"); + setNav(false); if (!$("#view").innerHTML) $("#view").innerHTML = loading(); RENDER[state.page](); } @@ -676,8 +682,11 @@ document.addEventListener("click", (e) => { const t = e.target; if (t.closest("#menu-btn")) { - const open = $("#nav").classList.toggle("open"); - $("#menu-btn").setAttribute("aria-expanded", String(open)); + setNav(!$("#nav").classList.contains("open")); + return; + } + if (t.closest("#nav-close, #nav-backdrop")) { + setNav(false); return; } if (t.closest("#signout")) { @@ -806,6 +815,7 @@ document.addEventListener("submit", async (e) => { }); document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && $("#nav").classList.contains("open")) { setNav(false); return; } if (e.key === "Escape" && $("#drawer-root").innerHTML && !$("#modal").open) closeDrawer(); }); diff --git a/internal/web/assets/public.js b/internal/web/assets/public.js index 1ffec26..dd0adf6 100644 --- a/internal/web/assets/public.js +++ b/internal/web/assets/public.js @@ -196,15 +196,12 @@ async function openPlayer(sid) {