From 0af93d1eabc3cbdfcb00e84bbe5d397a3ca6f6dc Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 29 Sep 2026 17:19:53 +0100 Subject: [PATCH] Rank log: inactive ranks (Guardian) count as promotions to and from, demotion when lost; Guardian is staff --- README.md | 10 +++++++--- internal/web/public.go | 20 ++++++++++++++++++-- internal/web/public_test.go | 25 +++++++++++++++++++++++++ internal/web/site.go | 19 +++++++++++-------- internal/web/staff.go | 3 ++- site.example.json | 16 ++++++++-------- sql/rank-log.sql | 7 +++++-- 7 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 internal/web/public_test.go diff --git a/README.md b/README.md index a599d6e..7dddc43 100644 --- a/README.md +++ b/README.md @@ -79,13 +79,17 @@ environment). SimpleAdmin groups have no display name or colour, so those come from a site config (`SAW_SITE_CONFIG`, see `site.example.json`). The built-in defaults cover `#rank/owner`, `senioradmin`, `admin`, `trialadmin`, `mod`, `trialmod`, `helper` and `guardian`. Ranks marked -`"supporter": true` are listed apart from staff and can't sign in unless they hold -`@css/generic`. +`"supporter": true` (VIPs) are listed apart from staff and don't count as staff online. Ranks +marked `"inactive": true` (Guardian: staff who are stepping back) are listed with the staff; they +only change how the rank log reads (see below). Nobody can sign in without `@css/generic`. ### Rank changes Every change to a player's rank (their highest-immunity group) is recorded in `sa_rank_log` and -listed under the public Staff page: who, from which rank to which, who changed it and when. The +listed under the public Staff page: who, from which rank to which, who changed it and when. A +change is a promotion or demotion by the ranks' immunity, except for inactive ranks: moving +between an inactive rank and any other rank is a promotion either way, and losing an inactive rank +is a demotion. The panel creates the table at startup (`sql/rank-log.sql`, embedded; run it by hand if the database user can't create tables). It logs changes made in the panel (giving, moving or removing a rank, and deleting a rank that people hold). Our SimpleAdmin fork (1.8.2b-zio10+) logs `css_addadmin`, diff --git a/internal/web/public.go b/internal/web/public.go index 2312e2b..24e9715 100644 --- a/internal/web/public.go +++ b/internal/web/public.go @@ -214,6 +214,20 @@ type rankChangeView struct { New *rankView `json:"new,omitempty"` } +// inactiveKind overrides the stored kind (which compares immunity) for changes to or from an +// inactive rank: moving between an inactive rank and any other rank is a promotion either way, +// and losing an inactive rank altogether is a demotion. +func inactiveKind(kind string, old, new *rankView) string { + oldIn, newIn := old != nil && old.Inactive, new != nil && new.Inactive + switch { + case old != nil && new != nil && oldIn != newIn: + return "promoted" + case oldIn && new == nil: + return "demoted" + } + return kind +} + // publicRankChanges is the promotions and demotions list, with each rank's label and colour. func (s *Server) publicRankChanges(w http.ResponseWriter, r *http.Request) { lim, off, num := page(r) @@ -232,11 +246,13 @@ func (s *Server) publicRankChanges(w http.ResponseWriter, r *http.Request) { return nil } rk := s.site.rank(g, x.groupIndex(g)) - return &rankView{Name: g, Label: rk.Label, Color: rk.Color, Supporter: rk.Supporter} + return &rankView{Name: g, Label: rk.Label, Color: rk.Color, Supporter: rk.Supporter, Inactive: rk.Inactive} } out := make([]rankChangeView, 0, len(items)) for _, c := range items { - out = append(out, rankChangeView{RankChange: c, Old: view(c.OldRank), New: view(c.NewRank)}) + v := rankChangeView{RankChange: c, Old: view(c.OldRank), New: view(c.NewRank)} + v.Kind = inactiveKind(v.Kind, v.Old, v.New) + out = append(out, v) } writeJSON(w, http.StatusOK, newList(out, total, num)) } diff --git a/internal/web/public_test.go b/internal/web/public_test.go new file mode 100644 index 0000000..3414575 --- /dev/null +++ b/internal/web/public_test.go @@ -0,0 +1,25 @@ +package web + +import "testing" + +func TestInactiveKind(t *testing.T) { + guardian := &rankView{Name: "#rank/guardian", Inactive: true} + mod := &rankView{Name: "#rank/mod"} + owner := &rankView{Name: "#rank/owner"} + for _, c := range []struct { + kind string + old, new *rankView + want string + }{ + {"demoted", owner, guardian, "promoted"}, + {"promoted", guardian, mod, "promoted"}, + {"removed", guardian, nil, "demoted"}, + {"added", nil, guardian, "added"}, + {"demoted", owner, mod, "demoted"}, + {"removed", mod, nil, "removed"}, + } { + if got := inactiveKind(c.kind, c.old, c.new); got != c.want { + t.Errorf("%v -> %v: got %s, want %s", c.old, c.new, got, c.want) + } + } +} diff --git a/internal/web/site.go b/internal/web/site.go index 4636430..0f75e22 100644 --- a/internal/web/site.go +++ b/internal/web/site.go @@ -20,20 +20,23 @@ type Rank struct { About string `json:"about"` // Supporter ranks (VIPs) are listed apart from staff and don't count as staff online. Supporter bool `json:"supporter"` + // Inactive ranks are for staff who are stepping back. In the rank log, moving between an + // inactive rank and any other rank is a promotion either way, and losing one is a demotion. + Inactive bool `json:"inactive"` } func defaultSite() Site { return Site{ Title: "zio.sh", Ranks: map[string]Rank{ - "#rank/owner": {Label: "Owner", Color: "#fe113d", About: "Runs the server."}, - "#rank/senioradmin": {Label: "Senior admin", Color: "#ff7a45", About: "Manages the staff team and server settings."}, - "#rank/admin": {Label: "Admin", Color: "#f0a63a", About: "Handles appeals and longer bans."}, - "#rank/trialadmin": {Label: "Trial admin", Color: "#e8d25a", About: "Admins on trial."}, - "#rank/mod": {Label: "Moderator", Color: "#37c58f", About: "Keeps matches fair. Can kick, gag and issue short bans."}, - "#rank/trialmod": {Label: "Trial mod", Color: "#4fb3d9", About: "Moderators on trial."}, - "#rank/helper": {Label: "Helper", Color: "#7c8cf5", About: "Answers questions and handles chat."}, - "#rank/guardian": {Label: "Guardian", Color: "#c77dff", About: "Supporters of the server. Not staff, and can't punish anyone.", Supporter: true}, + "#rank/owner": {Label: "Owner", Color: "#fe113d", About: "Owns the server and looks after everything behind the scenes."}, + "#rank/senioradmin": {Label: "Senior Admin", Color: "#ff7a45", About: "Looks after the staff team and helps out wherever they're needed."}, + "#rank/admin": {Label: "Admin", Color: "#f0a63a", About: "Handles bans and appeals, and steps in when things get out of hand."}, + "#rank/trialadmin": {Label: "Trial Admin", Color: "#e8d25a", About: "New to the admin team and still finding their feet."}, + "#rank/mod": {Label: "Moderator", Color: "#37c58f", About: "Keeps an eye on games and deals with anyone ruining them."}, + "#rank/trialmod": {Label: "Trial Mod", Color: "#4fb3d9", About: "New to the mod team and learning how things work."}, + "#rank/helper": {Label: "Helper", Color: "#7c8cf5", About: "Happy to answer questions and help you find your way around."}, + "#rank/guardian": {Label: "Guardian", Color: "#c77dff", About: "Staff taking a break from being active, but still part of the team.", Inactive: true}, }, } } diff --git a/internal/web/staff.go b/internal/web/staff.go index 3e5a1aa..8fbf14e 100644 --- a/internal/web/staff.go +++ b/internal/web/staff.go @@ -142,6 +142,7 @@ type rankView struct { Label string `json:"label"` Color string `json:"color"` Supporter bool `json:"supporter,omitempty"` + Inactive bool `json:"inactive,omitempty"` } func (s *Server) rankOf(x *staffIndex, steamid string) *rankView { @@ -154,7 +155,7 @@ func (s *Server) rankOf(x *staffIndex, steamid string) *rankView { return &rankView{Label: "Staff", Color: "#8a91a0"} } r := s.site.rank(g, x.groupIndex(g)) - return &rankView{Name: g, Label: r.Label, Color: r.Color, Supporter: r.Supporter} + return &rankView{Name: g, Label: r.Label, Color: r.Color, Supporter: r.Supporter, Inactive: r.Inactive} } // isStaff is true for admins whose rank isn't a supporter rank. diff --git a/site.example.json b/site.example.json index 13cf66e..077db5c 100644 --- a/site.example.json +++ b/site.example.json @@ -1,13 +1,13 @@ { "title": "zio.sh", "ranks": { - "#rank/owner": { "label": "Owner", "color": "#fe113d", "about": "Runs the server." }, - "#rank/senioradmin": { "label": "Senior admin", "color": "#ff7a45", "about": "Manages the staff team and server settings." }, - "#rank/admin": { "label": "Admin", "color": "#f0a63a", "about": "Handles appeals and longer bans." }, - "#rank/trialadmin": { "label": "Trial admin", "color": "#e8d25a", "about": "Admins on trial." }, - "#rank/mod": { "label": "Moderator", "color": "#37c58f", "about": "Keeps matches fair. Can kick, gag and issue short bans." }, - "#rank/trialmod": { "label": "Trial mod", "color": "#4fb3d9", "about": "Moderators on trial." }, - "#rank/helper": { "label": "Helper", "color": "#7c8cf5", "about": "Answers questions and handles chat." }, - "#rank/guardian": { "label": "Guardian", "color": "#c77dff", "about": "Supporters of the server. Not staff, and can't punish anyone.", "supporter": true } + "#rank/owner": { "label": "Owner", "color": "#fe113d", "about": "Owns the server and looks after everything behind the scenes." }, + "#rank/senioradmin": { "label": "Senior Admin", "color": "#ff7a45", "about": "Looks after the staff team and helps out wherever they're needed." }, + "#rank/admin": { "label": "Admin", "color": "#f0a63a", "about": "Handles bans and appeals, and steps in when things get out of hand." }, + "#rank/trialadmin": { "label": "Trial Admin", "color": "#e8d25a", "about": "New to the admin team and still finding their feet." }, + "#rank/mod": { "label": "Moderator", "color": "#37c58f", "about": "Keeps an eye on games and deals with anyone ruining them." }, + "#rank/trialmod": { "label": "Trial Mod", "color": "#4fb3d9", "about": "New to the mod team and learning how things work." }, + "#rank/helper": { "label": "Helper", "color": "#7c8cf5", "about": "Happy to answer questions and help you find your way around." }, + "#rank/guardian": { "label": "Guardian", "color": "#c77dff", "about": "Staff taking a break from being active, but still part of the team.", "inactive": true } } } diff --git a/sql/rank-log.sql b/sql/rank-log.sql index ce4e348..34e09a9 100644 --- a/sql/rank-log.sql +++ b/sql/rank-log.sql @@ -7,8 +7,11 @@ -- and the in-game admin menu. Nothing writes it for ranks that lapse when an admin's `ends` passes. -- -- kind is added, removed, promoted, demoted (compared by group immunity) or changed (same --- immunity). old_rank/new_rank are group names ("#rank/mod"), NULL for no rank. admin_steamid 0 --- is Console. created is a wall-clock value in SimpleAdmin's Timezone, like its other tables. +-- immunity). The panel overrides it when listing changes involving an inactive rank (one marked +-- "inactive" in its site config, like Guardian): moving between an inactive rank and any other +-- rank shows as promoted either way, and losing an inactive rank shows as demoted. old_rank/new_rank are group names ("#rank/mod"), NULL for no +-- rank. admin_steamid 0 is Console. created is a wall-clock value in SimpleAdmin's Timezone, like +-- its other tables. CREATE TABLE IF NOT EXISTS `sa_rank_log` ( `id` int(11) NOT NULL AUTO_INCREMENT,