Names on offline penalties, mobile menu close, player drawer tidy, IP-ban bypass flag
Bans and comms saved without a player name (SimpleAdmin's offline css_addban etc.) show the player's latest name from sa_players_ips, and the panel writes that name onto new rows it claims. The mobile sidebar closes from a Close button, the backdrop or Escape, since it covers the Menu button. The public player drawer drops "Right now" and "Free to play and talk." and puts the Steam profile link after the SteamID. Adds @css/bypassipban to the permission checkboxes.
This commit is contained in:
parent
657ead4c5a
commit
6324fc7f81
7 changed files with 45 additions and 16 deletions
|
|
@ -231,6 +231,14 @@ func escapeLike(s string) string {
|
||||||
return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s)
|
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.
|
// Bans lists sa_bans, newest first, with the total matching count.
|
||||||
func (s *Store) Bans(ctx context.Context, q Query) ([]Penalty, int, error) {
|
func (s *Store) Bans(ctx context.Context, q Query) ([]Penalty, int, error) {
|
||||||
where, args := s.where("sa_bans", q)
|
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 {
|
if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sa_bans"+where, args...).Scan(&total); err != nil {
|
||||||
return nil, 0, err
|
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,
|
b.admin_name, b.reason, b.duration, b.ends, b.created, b.status,
|
||||||
COALESCE(ua.player_name,''), COALESCE(u.reason,'')
|
COALESCE(ua.player_name,''), COALESCE(u.reason,'')
|
||||||
FROM (SELECT * FROM sa_bans` + where + ` ORDER BY id DESC LIMIT ? OFFSET ?) b
|
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 {
|
if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sa_mutes"+where, args...).Scan(&total); err != nil {
|
||||||
return nil, 0, err
|
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,
|
m.reason, m.duration, m.passed, m.ends, m.created, m.status,
|
||||||
COALESCE(ua.player_name,''), COALESCE(u.reason,'')
|
COALESCE(ua.player_name,''), COALESCE(u.reason,'')
|
||||||
FROM (SELECT * FROM sa_mutes` + where + ` ORDER BY id DESC LIMIT ? OFFSET ?) m
|
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)
|
// 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
|
// 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.
|
// 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)
|
n, _ := parseSID(steamid)
|
||||||
a, _ := parseSID(adminSteamID)
|
a, _ := parseSID(adminSteamID)
|
||||||
q := `SELECT id FROM ` + table + ` WHERE id > ? AND player_steamid = ? AND admin_steamid = 0`
|
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 {
|
for range 20 {
|
||||||
var id int64
|
var id int64
|
||||||
if err := s.db.QueryRowContext(ctx, q, args...).Scan(&id); err == nil {
|
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
|
return id
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ func (s *Server) addPenalty(w http.ResponseWriter, r *http.Request, id *Identity
|
||||||
if req.Kind == "comm" {
|
if req.Kind == "comm" {
|
||||||
commType = req.Type
|
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()
|
go s.poll.Refresh()
|
||||||
slog.Info("penalty issued", "admin", id.SteamID, "kind", req.Kind, "type", req.Type, "player", req.SteamID, "minutes", req.Duration)
|
slog.Info("penalty issued", "admin", id.SteamID, "kind", req.Kind, "type", req.Type, "player", req.SteamID, "minutes", req.Duration)
|
||||||
if claimed == 0 {
|
if claimed == 0 {
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,7 @@ var knownFlags = [][3]string{
|
||||||
{"@css/config", "Config", "Exec config files"},
|
{"@css/config", "Config", "Exec config files"},
|
||||||
{"@css/rcon", "RCON", "Run raw console commands"},
|
{"@css/rcon", "RCON", "Run raw console commands"},
|
||||||
{"@css/cheats", "Cheats", "sv_cheats-protected 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/vip", "VIP", "VIP perks in other plugins"},
|
||||||
{"@css/root", "Root", "Every permission, and managing ranks and staff"},
|
{"@css/root", "Root", "Every permission, and managing ranks and staff"},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<nav class="nav" id="nav" aria-label="Main">
|
<nav class="nav" id="nav" aria-label="Main">
|
||||||
|
<button class="btn sm ghost nav-close" type="button" id="nav-close" aria-label="Close menu">Close</button>
|
||||||
<a class="brand" href="#overview">
|
<a class="brand" href="#overview">
|
||||||
<svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true">
|
<svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true">
|
||||||
<path d="M16 2 28 7v9c0 7-5 12-12 14C9 28 4 23 4 16V7z" fill="none" stroke="#fe113d" stroke-width="2.5"/>
|
<path d="M16 2 28 7v9c0 7-5 12-12 14C9 28 4 23 4 16V7z" fill="none" stroke="#fe113d" stroke-width="2.5"/>
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
<button class="btn sm ghost" type="button" id="signout">Sign out</button>
|
<button class="btn sm ghost" type="button" id="signout">Sign out</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="nav-backdrop" id="nav-backdrop" aria-hidden="true"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mobile-bar">
|
<div class="mobile-bar">
|
||||||
|
|
|
||||||
|
|
@ -648,12 +648,18 @@ function renderNav() {
|
||||||
$("#nav-links").innerHTML = PAGES.map((p) => `<li><a class="link" href="#${p.id}" ${p.id === state.page ? 'aria-current="page"' : ""}>${p.label}${count[p.id] !== undefined && count[p.id] !== "" ? `<span class="count">${esc(count[p.id])}</span>` : ""}</a></li>`).join("");
|
$("#nav-links").innerHTML = PAGES.map((p) => `<li><a class="link" href="#${p.id}" ${p.id === state.page ? 'aria-current="page"' : ""}>${p.label}${count[p.id] !== undefined && count[p.id] !== "" ? `<span class="count">${esc(count[p.id])}</span>` : ""}</a></li>`).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() {
|
function route() {
|
||||||
const page = location.hash.slice(1);
|
const page = location.hash.slice(1);
|
||||||
state.page = RENDER[page] ? page : "overview";
|
state.page = RENDER[page] ? page : "overview";
|
||||||
renderNav();
|
renderNav();
|
||||||
$("#nav").classList.remove("open");
|
setNav(false);
|
||||||
$("#menu-btn").setAttribute("aria-expanded", "false");
|
|
||||||
if (!$("#view").innerHTML) $("#view").innerHTML = loading();
|
if (!$("#view").innerHTML) $("#view").innerHTML = loading();
|
||||||
RENDER[state.page]();
|
RENDER[state.page]();
|
||||||
}
|
}
|
||||||
|
|
@ -676,8 +682,11 @@ document.addEventListener("click", (e) => {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
|
|
||||||
if (t.closest("#menu-btn")) {
|
if (t.closest("#menu-btn")) {
|
||||||
const open = $("#nav").classList.toggle("open");
|
setNav(!$("#nav").classList.contains("open"));
|
||||||
$("#menu-btn").setAttribute("aria-expanded", String(open));
|
return;
|
||||||
|
}
|
||||||
|
if (t.closest("#nav-close, #nav-backdrop")) {
|
||||||
|
setNav(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (t.closest("#signout")) {
|
if (t.closest("#signout")) {
|
||||||
|
|
@ -806,6 +815,7 @@ document.addEventListener("submit", async (e) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("keydown", (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();
|
if (e.key === "Escape" && $("#drawer-root").innerHTML && !$("#modal").open) closeDrawer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,15 +196,12 @@ async function openPlayer(sid) {
|
||||||
<aside class="drawer" role="dialog" aria-modal="true" aria-label="${esc(name)}">
|
<aside class="drawer" role="dialog" aria-modal="true" aria-label="${esc(name)}">
|
||||||
<div class="drawer-head">
|
<div class="drawer-head">
|
||||||
<span class="avatar lg">${initial(name)}</span>
|
<span class="avatar lg">${initial(name)}</span>
|
||||||
<div><h2>${esc(name)}</h2><span class="sid">${esc(p.steamid)}</span>${p.rank ? `<div>${rankTag(p.rank)}</div>` : ""}</div>
|
<div><h2>${esc(name)}</h2><span class="sid">${esc(p.steamid)} · <a href="${steamProfile(p.steamid)}" target="_blank" rel="noopener">Steam profile</a></span>${p.rank ? `<div>${rankTag(p.rank)}</div>` : ""}</div>
|
||||||
<button class="close" type="button" data-close aria-label="Close">×</button>
|
<button class="close" type="button" data-close aria-label="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<section>
|
${now.length || p.online ? `<section>
|
||||||
<h3>Right now</h3>
|
<div class="chips">${p.online ? '<span class="tag">On the server</span>' : ""}${now.map((h) => `<span class="tag ${h.kind === "ban" ? "red" : "amber"}">${h.kind === "ban" ? "Banned" : COMM_HELP[h.type]}${h.duration ? `, ${fmtLeft(served(h).left)}` : ", permanently"}</span>`).join("")}</div>
|
||||||
${now.length ? `<div class="chips">${now.map((h) => `<span class="tag ${h.kind === "ban" ? "red" : "amber"}">${h.kind === "ban" ? "Banned" : COMM_HELP[h.type]}${h.duration ? `, ${fmtLeft(served(h).left)}` : ", permanently"}</span>`).join("")}</div>`
|
</section>` : ""}
|
||||||
: `<p class="muted" style="margin:0">${p.online ? "On the server, free to play and talk." : "Free to play and talk."}</p>`}
|
|
||||||
<p style="margin:12px 0 0"><a href="${steamProfile(p.steamid)}" target="_blank" rel="noopener">Steam profile</a></p>
|
|
||||||
</section>
|
|
||||||
<section>
|
<section>
|
||||||
<h3>History</h3>
|
<h3>History</h3>
|
||||||
${history.length ? `<ul class="history">${history.map((h) => `<li>
|
${history.length ? `<ul class="history">${history.map((h) => `<li>
|
||||||
|
|
|
||||||
|
|
@ -772,6 +772,7 @@ dialog::backdrop { background: rgba(5, 6, 9, 0.65); }
|
||||||
.empty { padding: 40px 18px; text-align: center; color: var(--muted); }
|
.empty { padding: 40px 18px; text-align: center; color: var(--muted); }
|
||||||
|
|
||||||
.mobile-bar { display: none; }
|
.mobile-bar { display: none; }
|
||||||
|
.nav-close, .nav-backdrop { display: none; }
|
||||||
|
|
||||||
/* ---------- Responsive ---------- */
|
/* ---------- Responsive ---------- */
|
||||||
|
|
||||||
|
|
@ -790,6 +791,14 @@ dialog::backdrop { background: rgba(5, 6, 9, 0.65); }
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.nav.open { transform: none; box-shadow: 0 0 60px rgba(0, 0, 0, 0.6); }
|
.nav.open { transform: none; box-shadow: 0 0 60px rgba(0, 0, 0, 0.6); }
|
||||||
|
.nav-close { display: inline-flex; align-self: flex-end; margin-bottom: -12px; }
|
||||||
|
.nav.open ~ .nav-backdrop {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 29;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
.mobile-bar {
|
.mobile-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue