Records: the most bomb plants, defuses, knife, Zeus and headshot kills for the last 7 days, 30 days or all time, and each player's counts, when PlaytimeHistory 0.2.0's pt_events is there. The public site's top bar now sticks the whole way down, Playtime comes after Server, and only the player drawer scrolls while it's open.
307 lines
18 KiB
Markdown
307 lines
18 KiB
Markdown
# simpleadmin-web
|
|
|
|
A SourceBans-style web panel for [CS2-SimpleAdmin](https://github.com/daffyyyy/CS2-SimpleAdmin)
|
|
(built against 1.8.2b's MySQL schema).
|
|
|
|
- **Public site** (`/`): the live server (name, map, players, team score and a scoreboard), bans,
|
|
gags and mutes, the staff list and its rank changes (promotions, demotions, who made them).
|
|
Read-only. It never shows who issued a penalty, IP addresses or internal immunity values.
|
|
- **Staff panel** (`/admin`): sign in through Steam. Ban, gag, mute, silence, warn and kick; lift
|
|
bans and blocks; manage ranks, permissions and staff; change server cvars and SimpleAdmin's
|
|
punishment settings.
|
|
- **Notifications** (staff): push notifications for new and lifted bans, gags, mutes and silences,
|
|
and the game server going down, on phones and computers, with quiet hours (see below).
|
|
- **Verification** (optional): who our GroupVerify plugin verifies, with overrides to give and
|
|
remove, and its settings. Only appears when GroupVerify is installed (see below).
|
|
- **Playtime** (optional): who plays the most, when the server is busy, and each player's time,
|
|
stats and maps, from our PlaytimeHistory plugin's database. Staff also see who's on with their
|
|
location, staff activity, disconnect reasons and players' name and session history. Only appears
|
|
when `SAW_PT_DSN` is set (see below).
|
|
|
|
Live data comes from the **WebPanelBridge** plugin (`plugins/WebPanelBridge` in this repo) over
|
|
RCON. Without it the panel still works, but shows the server as offline and can't kick or warn.
|
|
|
|
## How it works
|
|
|
|
- **Reading**: straight from SimpleAdmin's MySQL tables (`sa_bans`, `sa_mutes`, `sa_warns`,
|
|
`sa_admins`, `sa_groups` and their flag, unban and unmute tables, `sa_players_ips`).
|
|
- **Penalties**: sent over RCON as SimpleAdmin's own commands (`css_addban`, `css_addgag`,
|
|
`css_unban`, `css_warn`, …), so they take effect in game at once, with SimpleAdmin's usual
|
|
announcements, Discord messages and IP handling. SimpleAdmin records console commands as
|
|
"Console", so the panel then rewrites that row to credit the staff member who did it.
|
|
- **Game server unreachable**: bans and blocks are written to the database directly, the way
|
|
SimpleAdmin writes them. They apply when the player next joins (bans are also picked up by
|
|
SimpleAdmin within about a minute). Kicks and warnings need the server.
|
|
- **Ranks and staff**: written to the database, then `css_reloadadmins`. A rank is a SimpleAdmin
|
|
group (`#rank/mod`); staff are admins holding exactly one such group. The rank editor offers the
|
|
`@css/...` flags and our `@zio/...` ones (from the server's `admin_overrides.json` and our
|
|
SimpleAdmin fork); a new `@zio/` flag needs adding to `knownFlags` in `internal/web/admin.go`.
|
|
- **Permissions**: commands sent over RCON run as Console, which skips all of SimpleAdmin's own
|
|
checks, so the panel enforces them itself with the same flags:
|
|
|
|
| Action | Needs |
|
|
|---|---|
|
|
| Use the panel | `@css/generic` |
|
|
| Ban | `@css/ban`; permanent or longer than `MaxBanDuration` also `@css/permban` |
|
|
| Unban | `@css/unban` |
|
|
| Gag, mute, silence, lift them | `@css/chat`; permanent or longer than `MaxMuteDuration` also `@css/permmute` |
|
|
| Kick, warn | `@css/kick` |
|
|
| See IP addresses | `@css/showip` |
|
|
| Change map | `@css/changemap` |
|
|
| Server cvars | `@css/cvar` |
|
|
| Raw console commands (Server settings > Console; sent without waiting, output only logged) | `@css/rcon` |
|
|
| See the Staff & ranks page | `@zio/webpanel` |
|
|
| Give and remove verification overrides | `@zio/verify` (not on themselves, unless Root) |
|
|
| Staff playtime figures (Playtime page, player drawers, and on the public site when signed in) | `@css/generic` |
|
|
| PlaytimeHistory's own status on the Playtime page | `@css/root` |
|
|
| Ranks, staff, SimpleAdmin settings, reload admins, GroupVerify's group and on/off | `@css/root` |
|
|
|
|
`@css/root` grants everything. Nobody can act on a player whose immunity is higher than theirs,
|
|
or on themselves, or give a rank with more immunity than their own.
|
|
|
|
## Setup
|
|
|
|
1. Install WebPanelBridge on the game server (see its README).
|
|
2. Make sure RCON works: `rcon_password` set, and the panel can reach the game port over TCP.
|
|
SimpleAdmin stores the server's address and RCON password in `sa_servers`, and the panel uses
|
|
those unless `SAW_RCON_ADDR` / `SAW_RCON_PASSWORD` are set.
|
|
3. Copy `example.env` to `simpleadmin-web.env` and fill it in. `SAW_BASE_URL` must be the exact
|
|
public address; Steam returns signed-in users there.
|
|
4. Put the panel behind HTTPS (a reverse proxy). Session cookies are marked Secure when
|
|
`SAW_BASE_URL` is https.
|
|
|
|
Run it with podman:
|
|
|
|
```
|
|
podman build -t simpleadmin-web .
|
|
podman run -d --name simpleadmin-web --env-file simpleadmin-web.env \
|
|
--user 1000:1000 \
|
|
-v /srv/cs2/game/csgo/addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin:/data:Z \
|
|
-p 127.0.0.1:8080:8080 simpleadmin-web
|
|
```
|
|
|
|
`--user 1000:1000` matches the game container's user, which owns `CS2-SimpleAdmin.json`, so the
|
|
panel can edit it. Mount it with `:ro` instead to make those settings read-only. Changes to that
|
|
file apply the next time SimpleAdmin loads (a server restart). The panel keeps the previous
|
|
version as `CS2-SimpleAdmin.json.bak`. It writes keys in alphabetical order, so expect a reordered
|
|
file after the first save.
|
|
|
|
Or build and run it directly: `go build && ./simpleadmin-web` (Go 1.25, settings from the
|
|
environment).
|
|
|
|
### Rank names and colours
|
|
|
|
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` (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. 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`,
|
|
`css_deladmin` and the in-game admin menu to the same table, marked "in game". Not logged: ranks
|
|
that lapse when their expiry passes, `css_delgroup`, and edits made straight in the database.
|
|
History starts when the table is created.
|
|
|
|
### Verification (GroupVerify, optional)
|
|
|
|
[GroupVerify](https://git.zio.sh/cs2/GroupVerify) (`plugins/GroupVerify` in the cs2 repo) verifies
|
|
players who are in the server's Steam group or have a manual override, and other plugins give
|
|
verified players perks. It keeps everything in SimpleAdmin's database (`gv_verified`,
|
|
`gv_settings`), so the panel needs no setup for it:
|
|
|
|
- **Staff** get a Verification page as soon as GroupVerify's tables are in the database: whether
|
|
it's on, the plugin's state and version, the Steam group, its saved member list, and the
|
|
overrides. The player drawer shows why a player is or isn't verified. With `@zio/verify` (give
|
|
it to the ranks that should have it in the rank editor), staff give overrides for a day up to a
|
|
year or with no expiry, change them and remove them. Root sets the group and turns verification
|
|
on and off.
|
|
- **Everyone** sees a "Verified" tag on the scoreboard and on player pages, but only while
|
|
verification is on (enabled, with a group set) and GroupVerify isn't missing from the server's
|
|
plugin list. The public site never shows why someone is verified, or who gave an override.
|
|
- **Clan tag**: Root can set the scoreboard tag verified players without a rank get (GroupVerify's
|
|
`clan_tag`, at most 12 characters). CS2-Tags (1.0.4c-zio6+) applies it, and gives unverified
|
|
players without a rank no tag at all. Blank turns it off.
|
|
- **Whether it's loaded** comes from `css_plugins list` over RCON, read once a minute alongside
|
|
the live status. While the server is down it's unknown, and that doesn't hide anything.
|
|
- **Changes**: overrides are written to `gv_verified` and GroupVerify picks them up on its next
|
|
reload (`db_reload_seconds`, 60 by default). Settings are written to `gv_settings` and, when
|
|
the plugin is loaded, also sent as its own `css_gv_group`/`css_gv_enable`/`css_gv_disable`, so
|
|
they apply at once.
|
|
- **Times** in its tables are in GroupVerify's own `timezone` setting, which the panel follows.
|
|
Keep it the same as SimpleAdmin's `Timezone`.
|
|
|
|
GroupVerify (0.3.0+) checks each player with the Steam Web API as they join, so "Confirmed group
|
|
members" is everyone Steam has said is in the group when they joined, not the group's full member
|
|
list. The panel never asks Steam itself.
|
|
|
|
### Playtime (PlaytimeHistory, optional)
|
|
|
|
[PlaytimeHistory](https://git.zio.sh/cs2/PlaytimeHistory) (`plugins/PlaytimeHistory` in the cs2
|
|
repo) records sessions, team and alive/dead time, maps, stats, ping and location in **its own
|
|
database**, not SimpleAdmin's. To show it, give the panel a second connection, ideally as a user
|
|
that can only read:
|
|
|
|
```sql
|
|
CREATE USER 'pt_web'@'<panel host>' IDENTIFIED BY '<password>';
|
|
GRANT SELECT ON playtime_history.* TO 'pt_web'@'<panel host>';
|
|
```
|
|
|
|
```
|
|
SAW_PT_DSN=pt_web:<password>@tcp(<db-host>:3306)/playtime_history
|
|
```
|
|
|
|
- **Nothing shows** without `SAW_PT_DSN`, or while that database is down or doesn't have
|
|
PlaytimeHistory's tables. The panel checks once a minute and logs when that changes; it starts
|
|
and runs normally either way.
|
|
- **Public site**: a Playtime tab (once something's recorded) with the top players for the last 7
|
|
days, 30 days or all time (time on T or CT; spectating doesn't count), the last 7 days against
|
|
the 7 before, the busiest hours, players per day, a day-by-hour heatmap and maps. With
|
|
PlaytimeHistory 0.2.0+ (its `pt_events` table), also Records: the most bomb plants, defuses,
|
|
knife kills, Zeus kills and headshot kills for the same periods, counted as they happen, never in
|
|
warmup, and not counting kills of bots. Player pages get time played with the T/CT/spectating
|
|
split, the last 30 days, K/D, headshots, damage per round, MVPs, their record counts and
|
|
favourite maps.
|
|
- **Staff** with `@css/generic` (PlaytimeHistory's own default for looking up others) get, on the
|
|
staff panel's Playtime page and on the public site while signed in: who's on now with how long,
|
|
team, ping and location; each staff member's time in the last 7 or 30 days; disconnect reasons;
|
|
countries; and in player drawers, the names they've used, recent sessions, location and ping.
|
|
Root also sees whether the plugin is loaded and what `css_pt_status` says. Who's on comes from
|
|
the live status, so admins hidden with `css_hide` stay hidden.
|
|
- **Days and hours of the day** are in the viewer's own time zone (the browser sends it). An hour
|
|
goes to the day it starts on, which is exact for zones a whole number of hours from UTC.
|
|
- **Locations** come from DB-IP City Lite, credited (CC BY 4.0) wherever they're shown. No IP
|
|
addresses are stored.
|
|
- `SAW_PT_SERVER_ID` limits it to one `pt_servers.id` when several servers share the database.
|
|
All-time totals (pt_players) always cover every server.
|
|
|
|
### Name history (optional)
|
|
|
|
SimpleAdmin keeps one row per SteamID + IP in `sa_players_ips` and overwrites its name on each
|
|
connect, so a rename on the same IP loses the old name. `sql/players-ips-name-history.sql` adds
|
|
`name` to that table's primary key, so every name a SteamID has joined with is kept and shows up
|
|
on its player page and in name search. SimpleAdmin needs no changes. Run it once against the
|
|
SimpleAdmin database; the file has the revert. Names are still only recorded at connect.
|
|
|
|
### Installing as an app
|
|
|
|
The site is a PWA, so Chrome on Android (and desktop Chrome or Edge) offers to install it: menu >
|
|
**Add to Home screen** / **Install app**. The public site and the staff panel install as separate
|
|
apps (`/manifest.webmanifest` opens on `/`, `/admin.webmanifest` on `/admin`); install the staff one
|
|
from the staff panel or its sign-in page. Installing needs the site on HTTPS. The service worker
|
|
(`assets/sw.js`, served at `/sw.js`) caches only an offline page, never data.
|
|
|
|
The icons are rendered from `assets/icon.svg` and `assets/icon-maskable.svg` (the brand mark kept
|
|
inside the maskable safe zone). After changing either:
|
|
|
|
```
|
|
cd internal/web/assets
|
|
rsvg-convert -w 192 -h 192 icon.svg -o icon-192.png
|
|
rsvg-convert -w 512 -h 512 icon.svg -o icon-512.png
|
|
rsvg-convert -w 512 -h 512 icon-maskable.svg -o icon-maskable-512.png
|
|
rsvg-convert -w 180 -h 180 icon-maskable.svg -o apple-touch-icon.png
|
|
```
|
|
|
|
### Notifications
|
|
|
|
Staff turn notifications on per device from the panel's **Notifications** page, and pick which
|
|
types they want and their quiet hours there; those apply to all their devices. It's standard Web
|
|
Push: it works in Chrome, Edge, Firefox and Safari (on iPhone only from a home-screen install), and
|
|
in the Android app, where they show as the app's own.
|
|
|
|
- **Types**: ban added or lifted, and the same for gags, mutes and silences, each on its own
|
|
switch; and the game server down (no RCON answer for 30 seconds) and back up. "Live score" and
|
|
"Admin called" are listed but switched off until they're built (admin calls need a plugin).
|
|
- **What's watched**: SimpleAdmin's `sa_bans`, `sa_mutes`, `sa_unbans` and `sa_unmutes`, every 5
|
|
seconds, so penalties made in game are sent too, not just ones from the panel. New rows wait 7
|
|
seconds first, so panel actions are credited to the staff member rather than "Console".
|
|
- **Who gets them**: staff who can use the panel (`@css/generic`), checked on every send, so people
|
|
removed from staff stop getting them at once. Nobody is told about their own actions.
|
|
- **Links**: tapping a penalty notification, or its **View player** button, opens the public ban or
|
|
gag/mute list searching for the player (`/#bans?q=<steamid64>`, `/#comms?q=...`).
|
|
- **Setup**: none. The panel creates `saw_push_devices`, `saw_push_prefs` and `saw_settings` at
|
|
startup (`sql/push.sql`), and makes its VAPID key on first start, kept in `saw_settings`. Deleting
|
|
that key stops every device until it's turned on again. Messages are encrypted for each device
|
|
(RFC 8291) and only sent to the browsers' own push services (Google, Mozilla, Apple, Microsoft).
|
|
|
|
### Android app
|
|
|
|
`android/` is the site as an Android app: a Trusted Web Activity, which opens the public site in
|
|
the phone's Chrome, full screen, with no code of its own (Google's `androidbrowserhelper` does it
|
|
all). Build it with podman:
|
|
|
|
```
|
|
android/build.sh
|
|
```
|
|
|
|
That builds an image with JDK 17, the Android SDK and Gradle, and writes a signed
|
|
`android/out/simpleadmin-web.apk`. The signing key is kept outside the repo, in
|
|
`~/.config/simpleadmin-web/android/` (`release.jks` and its `password`), and made on the first run.
|
|
Back it up: an APK signed with another key won't install over the old one. Then:
|
|
|
|
- Set `SAW_ANDROID_PACKAGE` and `SAW_ANDROID_CERT_SHA256` (the fingerprint `build.sh` prints) so
|
|
the panel serves `/.well-known/assetlinks.json`. Without it, Chrome shows its URL bar over the app.
|
|
- Bump `versionCode` in `android/app/build.gradle` for every APK you hand out.
|
|
- The site and start page are at the top of `android/app/build.gradle`; never change `applicationId`.
|
|
|
|
The app needs Chrome (or another browser with TWA support) on the phone. Notifications from the
|
|
panel are delegated to the app (`DelegationService`), so they show under its name and icon, and
|
|
Android 13+ asks for the app's notification permission when they're turned on.
|
|
|
|
## Limitations and caveats
|
|
|
|
- **Tested against a real MySQL 8.4** with SimpleAdmin 1.8.2b's migrations applied, and a fake game
|
|
server that speaks Source RCON and imitates SimpleAdmin's console commands. **Not yet tested
|
|
against a live CS2 server or live Steam sign-in.** Things to check on first run: that RCON
|
|
returns WebPanelBridge's output (it prints through `Server.PrintToConsole`, like `css version`),
|
|
and that the cvar values on the settings page read correctly.
|
|
- In-game announcements for panel actions name "Console", because that's who SimpleAdmin sees.
|
|
The database records the real staff member.
|
|
- Unban and lift act on the player: `css_unban <steamid>` lifts every active ban on that SteamID,
|
|
as it does in game.
|
|
- Bans without a SteamID (IP-only bans from `css_banip`) are listed but can only be lifted in game.
|
|
- One server per panel. Set `SAW_SERVER_ID` if `sa_servers` lists several.
|
|
- Times are read in SimpleAdmin's `Timezone` (UTC by default), matching how it writes them.
|
|
|
|
## Development
|
|
|
|
```
|
|
go vet ./... && go test ./...
|
|
```
|
|
|
|
`internal/web/verify_db_test.go` also runs the Verification endpoints against a real MySQL when
|
|
`SAW_TEST_DSN` is set. It empties SimpleAdmin's admin and group tables and GroupVerify's, so give
|
|
it a scratch database holding SimpleAdmin 1.8.2b's migrations and GroupVerify's
|
|
`Migrations/001_CreateTables.sql`:
|
|
|
|
```
|
|
podman run -d --rm --name sa-test -e MYSQL_ROOT_PASSWORD=test -e MYSQL_DATABASE=sa -p 127.0.0.1:33061:3306 mysql:8.4
|
|
# apply the migrations, then:
|
|
SAW_TEST_DSN='root:test@tcp(127.0.0.1:33061)/sa?parseTime=true&loc=UTC' go test ./internal/web -run VerifyDB
|
|
```
|
|
|
|
`internal/playtime/playtime_db_test.go` runs the playtime queries against a real MySQL or MariaDB
|
|
when `SAW_TEST_PT_DSN` is set. It drops and recreates PlaytimeHistory's tables from
|
|
`internal/playtime/testdata/schema.sql` (a copy of the plugin's migration; keep it in step), so
|
|
give it a scratch database:
|
|
|
|
```
|
|
podman run -d --rm --name pt-test -e MARIADB_ROOT_PASSWORD=test -e MARIADB_DATABASE=playtime_history -p 127.0.0.1:33071:3306 mariadb:11
|
|
SAW_TEST_PT_DSN='root:test@tcp(127.0.0.1:33071)/playtime_history' go test ./internal/playtime -run DB
|
|
```
|
|
|
|
`internal/web/notify_db_test.go` does the same for notifications (`-run NotifyDB`): the endpoints,
|
|
and the watcher sending to a fake push service. It needs only SimpleAdmin's migrations, and empties
|
|
its ban, mute, admin and group tables and the panel's push tables.
|
|
|
|
The pages are plain HTML, CSS and JS in `internal/web/assets`, embedded into the binary. The
|
|
design mockup they came from is in `docs/mockup` at the repo root.
|