diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml
new file mode 100644
index 0000000000..8feec273b4
--- /dev/null
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
new file mode 100644
index 0000000000..824d9eb6c7
--- /dev/null
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
@@ -0,0 +1,132 @@
+using Content.Client.Administration.Managers;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Administration;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Network;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.UI.PlayerPanel;
+
+[GenerateTypedNameReferences]
+public sealed partial class PlayerPanel : FancyWindow
+{
+ private readonly IClientAdminManager _adminManager;
+
+ public event Action? OnUsernameCopy;
+ public event Action? OnOpenNotes;
+ public event Action? OnOpenBans;
+ public event Action? OnAhelp;
+ public event Action? OnKick;
+ public event Action? OnOpenBanPanel;
+ public event Action? OnWhitelistToggle;
+ public event Action? OnFreezeAndMuteToggle;
+ public event Action? OnFreeze;
+ public event Action? OnLogs;
+ public event Action? OnDelete;
+ public event Action? OnRejuvenate;
+
+ public NetUserId? TargetPlayer;
+ public string? TargetUsername;
+ private bool _isWhitelisted;
+
+ public PlayerPanel(IClientAdminManager adminManager)
+ {
+ RobustXamlLoader.Load(this);
+ _adminManager = adminManager;
+
+ UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? "");
+ BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
+ KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
+ NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
+ ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
+ AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
+ WhitelistToggle.OnPressed += _ =>
+ {
+ OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
+ SetWhitelisted(!_isWhitelisted);
+ };
+ FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
+ FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
+ LogsButton.OnPressed += _ => OnLogs?.Invoke();
+ DeleteButton.OnPressed += _ => OnDelete?.Invoke();
+ RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
+ }
+
+ public void SetUsername(string player)
+ {
+ Title = Loc.GetString("player-panel-title", ("player", player));
+ PlayerName.Text = Loc.GetString("player-panel-username", ("player", player));
+ }
+
+ public void SetWhitelisted(bool? whitelisted)
+ {
+ if (whitelisted == null)
+ {
+ Whitelisted.Text = null;
+ WhitelistToggle.Visible = false;
+ }
+ else
+ {
+ Whitelisted.Text = Loc.GetString("player-panel-whitelisted");
+ WhitelistToggle.Text = whitelisted.Value.ToString();
+ WhitelistToggle.Visible = true;
+ _isWhitelisted = whitelisted.Value;
+ }
+ }
+
+ public void SetBans(int? totalBans, int? totalRoleBans)
+ {
+ // If one value exists then so should the other.
+ DebugTools.Assert(totalBans.HasValue && totalRoleBans.HasValue || totalBans == null && totalRoleBans == null);
+
+ Bans.Text = totalBans != null ? Loc.GetString("player-panel-bans", ("totalBans", totalBans)) : null;
+
+ RoleBans.Text = totalRoleBans != null ? Loc.GetString("player-panel-rolebans", ("totalRoleBans", totalRoleBans)) : null;
+ }
+
+ public void SetNotes(int? totalNotes)
+ {
+ Notes.Text = totalNotes != null ? Loc.GetString("player-panel-notes", ("totalNotes", totalNotes)) : null;
+ }
+
+ public void SetSharedConnections(int sharedConnections)
+ {
+ SharedConnections.Text = Loc.GetString("player-panel-shared-connections", ("sharedConnections", sharedConnections));
+ }
+
+ public void SetPlaytime(TimeSpan playtime)
+ {
+ Playtime.Text = Loc.GetString("player-panel-playtime",
+ ("days", playtime.Days),
+ ("hours", playtime.Hours % 24),
+ ("minutes", playtime.Minutes % (24 * 60)));
+ }
+
+ public void SetFrozen(bool canFreeze, bool frozen)
+ {
+ FreezeAndMuteToggleButton.Disabled = !canFreeze;
+ FreezeButton.Disabled = !canFreeze || frozen;
+
+ FreezeAndMuteToggleButton.Text = Loc.GetString(!frozen ? "player-panel-freeze-and-mute" : "player-panel-unfreeze");
+ }
+
+ public void SetAhelp(bool canAhelp)
+ {
+ AhelpButton.Disabled = !canAhelp;
+ }
+
+ public void SetButtons()
+ {
+ BanButton.Disabled = !_adminManager.CanCommand("banpanel");
+ KickButton.Disabled = !_adminManager.CanCommand("kick");
+ NotesButton.Disabled = !_adminManager.CanCommand("adminnotes");
+ ShowBansButton.Disabled = !_adminManager.CanCommand("banlist");
+ WhitelistToggle.Disabled =
+ !(_adminManager.CanCommand("addwhitelist") && _adminManager.CanCommand("removewhitelist"));
+ LogsButton.Disabled = !_adminManager.CanCommand("adminlogs");
+ RejuvenateButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);
+ DeleteButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);
+ }
+}
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs
new file mode 100644
index 0000000000..87ce756046
--- /dev/null
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs
@@ -0,0 +1,72 @@
+using Content.Client.Administration.Managers;
+using Content.Client.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+using Robust.Client.Console;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Administration.UI.PlayerPanel;
+
+[UsedImplicitly]
+public sealed class PlayerPanelEui : BaseEui
+{
+ [Dependency] private readonly IClientConsoleHost _console = default!;
+ [Dependency] private readonly IClientAdminManager _admin = default!;
+ [Dependency] private readonly IClipboardManager _clipboard = default!;
+
+ private PlayerPanel PlayerPanel { get; }
+
+ public PlayerPanelEui()
+ {
+ PlayerPanel = new PlayerPanel(_admin);
+
+ PlayerPanel.OnUsernameCopy += username => _clipboard.SetText(username);
+ PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\"");
+ // Kick command does not support GUIDs
+ PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\"");
+ PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\"");
+ PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\"");
+ PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\"");
+ PlayerPanel.OnWhitelistToggle += (id, whitelisted) =>
+ {
+ _console.ExecuteCommand(whitelisted ? $"whitelistremove \"{id}\"" : $"whitelistadd \"{id}\"");
+ };
+
+ PlayerPanel.OnFreezeAndMuteToggle += () => SendMessage(new PlayerPanelFreezeMessage(true));
+ PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage());
+ PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
+ PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
+ PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
+
+ PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());
+ }
+
+ public override void Opened()
+ {
+ PlayerPanel.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ PlayerPanel.Close();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not PlayerPanelEuiState s)
+ return;
+
+ PlayerPanel.TargetPlayer = s.Guid;
+ PlayerPanel.TargetUsername = s.Username;
+ PlayerPanel.SetUsername(s.Username);
+ PlayerPanel.SetPlaytime(s.Playtime);
+ PlayerPanel.SetBans(s.TotalBans, s.TotalRoleBans);
+ PlayerPanel.SetNotes(s.TotalNotes);
+ PlayerPanel.SetWhitelisted(s.Whitelisted);
+ PlayerPanel.SetSharedConnections(s.SharedConnections);
+ PlayerPanel.SetFrozen(s.CanFreeze, s.Frozen);
+ PlayerPanel.SetAhelp(s.CanAhelp);
+ PlayerPanel.SetButtons();
+ }
+}
diff --git a/Content.Server/Administration/Commands/PlayerPanelCommand.cs b/Content.Server/Administration/Commands/PlayerPanelCommand.cs
new file mode 100644
index 0000000000..4a065bd58a
--- /dev/null
+++ b/Content.Server/Administration/Commands/PlayerPanelCommand.cs
@@ -0,0 +1,56 @@
+using System.Linq;
+using Content.Server.EUI;
+using Content.Shared.Administration;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+
+namespace Content.Server.Administration.Commands;
+
+[AdminCommand(AdminFlags.Admin)]
+public sealed class PlayerPanelCommand : LocalizedCommands
+{
+ [Dependency] private readonly IPlayerLocator _locator = default!;
+ [Dependency] private readonly EuiManager _euis = default!;
+ [Dependency] private readonly IPlayerManager _players = default!;
+
+ public override string Command => "playerpanel";
+
+ public override async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (shell.Player is not { } admin)
+ {
+ shell.WriteError(Loc.GetString("cmd-playerpanel-server"));
+ return;
+ }
+
+ if (args.Length != 1)
+ {
+ shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-arguments"));
+ return;
+ }
+
+ var queriedPlayer = await _locator.LookupIdByNameOrIdAsync(args[0]);
+
+ if (queriedPlayer == null)
+ {
+ shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-player"));
+ return;
+ }
+
+ var ui = new PlayerPanelEui(queriedPlayer);
+ _euis.OpenEui(ui, admin);
+ ui.SetPlayerState();
+ }
+
+ public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
+
+ return CompletionResult.FromHintOptions(options, LocalizationManager.GetString("cmd-playerpanel-completion"));
+ }
+
+ return CompletionResult.Empty;
+ }
+}
diff --git a/Content.Server/Administration/PlayerPanelEui.cs b/Content.Server/Administration/PlayerPanelEui.cs
new file mode 100644
index 0000000000..4c0df80601
--- /dev/null
+++ b/Content.Server/Administration/PlayerPanelEui.cs
@@ -0,0 +1,210 @@
+using System.Linq;
+using Content.Server.Administration.Logs;
+using Content.Server.Administration.Managers;
+using Content.Server.Administration.Notes;
+using Content.Server.Administration.Systems;
+using Content.Server.Database;
+using Content.Server.EUI;
+using Content.Shared.Administration;
+using Content.Shared.Database;
+using Content.Shared.Eui;
+using Robust.Server.Player;
+using Robust.Shared.Player;
+
+namespace Content.Server.Administration;
+
+public sealed class PlayerPanelEui : BaseEui
+{
+ [Dependency] private readonly IAdminManager _admins = default!;
+ [Dependency] private readonly IServerDbManager _db = default!;
+ [Dependency] private readonly IAdminNotesManager _notesMan = default!;
+ [Dependency] private readonly IEntityManager _entity = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly EuiManager _eui = default!;
+ [Dependency] private readonly IAdminLogManager _adminLog = default!;
+
+ private readonly LocatedPlayerData _targetPlayer;
+ private int? _notes;
+ private int? _bans;
+ private int? _roleBans;
+ private int _sharedConnections;
+ private bool? _whitelisted;
+ private TimeSpan _playtime;
+ private bool _frozen;
+ private bool _canFreeze;
+ private bool _canAhelp;
+
+ public PlayerPanelEui(LocatedPlayerData player)
+ {
+ IoCManager.InjectDependencies(this);
+ _targetPlayer = player;
+ }
+
+ public override void Opened()
+ {
+ base.Opened();
+ _admins.OnPermsChanged += OnPermsChanged;
+ }
+
+ public override void Closed()
+ {
+ base.Closed();
+ _admins.OnPermsChanged -= OnPermsChanged;
+ }
+
+ public override EuiStateBase GetNewState()
+ {
+ return new PlayerPanelEuiState(_targetPlayer.UserId,
+ _targetPlayer.Username,
+ _playtime,
+ _notes,
+ _bans,
+ _roleBans,
+ _sharedConnections,
+ _whitelisted,
+ _canFreeze,
+ _frozen,
+ _canAhelp);
+ }
+
+ private void OnPermsChanged(AdminPermsChangedEventArgs args)
+ {
+ if (args.Player != Player)
+ return;
+
+ SetPlayerState();
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ ICommonSession? session;
+
+ switch (msg)
+ {
+ case PlayerPanelFreezeMessage freezeMsg:
+ if (!_admins.IsAdmin(Player) ||
+ !_entity.TrySystem(out var frozenSystem) ||
+ !_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
+ session.AttachedEntity == null)
+ return;
+
+ if (_entity.HasComponent(session.AttachedEntity))
+ {
+ _adminLog.Add(LogType.Action,$"{Player:actor} unfroze {_entity.ToPrettyString(session.AttachedEntity):subject}");
+ _entity.RemoveComponent(session.AttachedEntity.Value);
+ SetPlayerState();
+ return;
+ }
+
+ if (freezeMsg.Mute)
+ {
+ _adminLog.Add(LogType.Action,$"{Player:actor} froze and muted {_entity.ToPrettyString(session.AttachedEntity):subject}");
+ frozenSystem.FreezeAndMute(session.AttachedEntity.Value);
+ }
+ else
+ {
+ _adminLog.Add(LogType.Action,$"{Player:actor} froze {_entity.ToPrettyString(session.AttachedEntity):subject}");
+ _entity.EnsureComponent(session.AttachedEntity.Value);
+ }
+ SetPlayerState();
+ break;
+
+ case PlayerPanelLogsMessage:
+ if (!_admins.HasAdminFlag(Player, AdminFlags.Logs))
+ return;
+
+ _adminLog.Add(LogType.Action, $"{Player:actor} opened logs on {_targetPlayer.Username:subject}");
+ var ui = new AdminLogsEui();
+ _eui.OpenEui(ui, Player);
+ ui.SetLogFilter(search: _targetPlayer.Username);
+ break;
+ case PlayerPanelDeleteMessage:
+ case PlayerPanelRejuvenationMessage:
+ if (!_admins.HasAdminFlag(Player, AdminFlags.Debug) ||
+ !_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
+ session.AttachedEntity == null)
+ return;
+
+ if (msg is PlayerPanelRejuvenationMessage)
+ {
+ _adminLog.Add(LogType.Action,$"{Player:actor} rejuvenated {_entity.ToPrettyString(session.AttachedEntity):subject}");
+ if (!_entity.TrySystem(out var rejuvenate))
+ return;
+
+ rejuvenate.PerformRejuvenate(session.AttachedEntity.Value);
+ }
+ else
+ {
+ _adminLog.Add(LogType.Action,$"{Player:actor} deleted {_entity.ToPrettyString(session.AttachedEntity):subject}");
+ _entity.DeleteEntity(session.AttachedEntity);
+ }
+ break;
+ }
+ }
+
+ public async void SetPlayerState()
+ {
+ if (!_admins.IsAdmin(Player))
+ {
+ Close();
+ return;
+ }
+
+ _playtime = (await _db.GetPlayTimes(_targetPlayer.UserId))
+ .Where(p => p.Tracker == "Overall")
+ .Select(p => p.TimeSpent)
+ .FirstOrDefault();
+
+ if (_notesMan.CanView(Player))
+ {
+ _notes = (await _notesMan.GetAllAdminRemarks(_targetPlayer.UserId)).Count;
+ }
+ else
+ {
+ _notes = null;
+ }
+
+ _sharedConnections = _player.Sessions.Count(s => s.Channel.RemoteEndPoint.Address.Equals(_targetPlayer.LastAddress) && s.UserId != _targetPlayer.UserId);
+
+ // Apparently the Bans flag is also used for whitelists
+ if (_admins.HasAdminFlag(Player, AdminFlags.Ban))
+ {
+ _whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
+ // This won't get associated ip or hwid bans but they were not placed on this account anyways
+ _bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
+ // Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
+ // The only way to distinguish whether a role ban is the same is to compare the ban time.
+ // This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
+ _roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
+ }
+ else
+ {
+ _whitelisted = null;
+ _bans = null;
+ _roleBans = null;
+ }
+
+ if (_player.TryGetSessionById(_targetPlayer.UserId, out var session))
+ {
+ _canFreeze = session.AttachedEntity != null;
+ _frozen = _entity.HasComponent(session.AttachedEntity);
+ }
+ else
+ {
+ _canFreeze = false;
+ }
+
+ if (_admins.HasAdminFlag(Player, AdminFlags.Adminhelp))
+ {
+ _canAhelp = true;
+ }
+ else
+ {
+ _canAhelp = false;
+ }
+
+ StateDirty();
+ }
+}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs
index 3c8f7cd553..6198611ac8 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs
@@ -208,6 +208,15 @@ namespace Content.Server.Administration.Systems
ConfirmationPopup = true,
Impact = LogImpact.High,
});
+
+ // PlayerPanel
+ args.Verbs.Add(new Verb
+ {
+ Text = Loc.GetString("admin-player-actions-player-panel"),
+ Category = VerbCategory.Admin,
+ Act = () => _console.ExecuteCommand(player, $"playerpanel \"{targetActor.PlayerSession.UserId}\""),
+ Impact = LogImpact.Low
+ });
}
// Freeze
diff --git a/Content.Server/Whitelist/WhitelistCommands.cs b/Content.Server/Whitelist/WhitelistCommands.cs
index ab06130621..09df1d1419 100644
--- a/Content.Server/Whitelist/WhitelistCommands.cs
+++ b/Content.Server/Whitelist/WhitelistCommands.cs
@@ -27,7 +27,7 @@ public sealed class AddWhitelistCommand : LocalizedCommands
var loc = IoCManager.Resolve();
var name = string.Join(' ', args).Trim();
- var data = await loc.LookupIdByNameAsync(name);
+ var data = await loc.LookupIdByNameOrIdAsync(name);
if (data != null)
{
@@ -76,7 +76,7 @@ public sealed class RemoveWhitelistCommand : LocalizedCommands
var loc = IoCManager.Resolve();
var name = string.Join(' ', args).Trim();
- var data = await loc.LookupIdByNameAsync(name);
+ var data = await loc.LookupIdByNameOrIdAsync(name);
if (data != null)
{
diff --git a/Content.Shared/Administration/PlayerPanelEuiState.cs b/Content.Shared/Administration/PlayerPanelEuiState.cs
new file mode 100644
index 0000000000..186b992e4e
--- /dev/null
+++ b/Content.Shared/Administration/PlayerPanelEuiState.cs
@@ -0,0 +1,54 @@
+using Content.Shared.Eui;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+using YamlDotNet.Serialization.Callbacks;
+
+namespace Content.Shared.Administration;
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelEuiState(NetUserId guid,
+ string username,
+ TimeSpan playtime,
+ int? totalNotes,
+ int? totalBans,
+ int? totalRoleBans,
+ int sharedConnections,
+ bool? whitelisted,
+ bool canFreeze,
+ bool frozen,
+ bool canAhelp)
+ : EuiStateBase
+{
+ public readonly NetUserId Guid = guid;
+ public readonly string Username = username;
+ public readonly TimeSpan Playtime = playtime;
+ public readonly int? TotalNotes = totalNotes;
+ public readonly int? TotalBans = totalBans;
+ public readonly int? TotalRoleBans = totalRoleBans;
+ public readonly int SharedConnections = sharedConnections;
+ public readonly bool? Whitelisted = whitelisted;
+ public readonly bool CanFreeze = canFreeze;
+ public readonly bool Frozen = frozen;
+ public readonly bool CanAhelp = canAhelp;
+}
+
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelFreezeMessage : EuiMessageBase
+{
+ public readonly bool Mute;
+
+ public PlayerPanelFreezeMessage(bool mute = false)
+ {
+ Mute = mute;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelLogsMessage : EuiMessageBase;
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelDeleteMessage : EuiMessageBase;
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelRejuvenationMessage: EuiMessageBase;
diff --git a/Resources/Locale/en-US/administration/ui/actions.ftl b/Resources/Locale/en-US/administration/ui/actions.ftl
index 3c13d56c94..9893eb59af 100644
--- a/Resources/Locale/en-US/administration/ui/actions.ftl
+++ b/Resources/Locale/en-US/administration/ui/actions.ftl
@@ -7,6 +7,7 @@ admin-player-actions-ahelp = AHelp
admin-player-actions-respawn = Respawn
admin-player-actions-spawn = Spawn here
admin-player-spawn-failed = Failed to find valid coordinates
+admin-player-actions-player-panel = Open Player Panel
admin-player-actions-clone = Clone
admin-player-actions-follow = Follow
diff --git a/Resources/Locale/en-US/administration/ui/player-panel.ftl b/Resources/Locale/en-US/administration/ui/player-panel.ftl
new file mode 100644
index 0000000000..ed63dd6d10
--- /dev/null
+++ b/Resources/Locale/en-US/administration/ui/player-panel.ftl
@@ -0,0 +1,22 @@
+player-panel-title = information for {$player}
+player-panel-username = Username: {$player}
+player-panel-whitelisted = Whitelisted:
+player-panel-bans = Total Bans: {$totalBans}
+player-panel-rolebans = Total Role Bans: {$totalRoleBans}
+player-panel-notes = Total Notes: {$totalNotes}
+player-panel-playtime = Total Playtime: {$days}d:{$hours}h:{$minutes}m
+player-panel-shared-connections = Shared Connections: {$sharedConnections}
+
+player-panel-copy-username = Copy
+player-panel-show-notes = Notes
+player-panel-show-bans = Show Bans
+player-panel-help = Ahelp
+player-panel-freeze-and-mute = Freeze & Mute
+player-panel-freeze = Freeze
+player-panel-unfreeze = Unfreeze
+player-panel-kick = Kick
+player-panel-ban = Ban
+player-panel-logs = Logs
+player-panel-delete = Delete
+player-panel-rejuvenate = Rejuvenate
+player-panel-false = False
diff --git a/Resources/Locale/en-US/connection-messages.ftl b/Resources/Locale/en-US/connection-messages.ftl
index f1596d9015..309e8fc2f8 100644
--- a/Resources/Locale/en-US/connection-messages.ftl
+++ b/Resources/Locale/en-US/connection-messages.ftl
@@ -11,14 +11,14 @@ whitelist-playercount-invalid = {$min ->
whitelist-not-whitelisted-rp = You are not whitelisted. To become whitelisted, visit our Discord (which can be found at https://spacestation14.io) and check the #rp-whitelist channel.
cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist.
-cmd-whitelistadd-help = Usage: whitelistadd
+cmd-whitelistadd-help = Usage: whitelistadd
cmd-whitelistadd-existing = {$username} is already on the whitelist!
cmd-whitelistadd-added = {$username} added to the whitelist
cmd-whitelistadd-not-found = Unable to find '{$username}'
cmd-whitelistadd-arg-player = [player]
cmd-whitelistremove-desc = Removes the player with the given username from the server whitelist.
-cmd-whitelistremove-help = Usage: whitelistremove
+cmd-whitelistremove-help = Usage: whitelistremove
cmd-whitelistremove-existing = {$username} is not on the whitelist!
cmd-whitelistremove-removed = {$username} removed from the whitelist
cmd-whitelistremove-not-found = Unable to find '{$username}'
diff --git a/Resources/Locale/en-US/info/playerpanel.ftl b/Resources/Locale/en-US/info/playerpanel.ftl
new file mode 100644
index 0000000000..138939c48c
--- /dev/null
+++ b/Resources/Locale/en-US/info/playerpanel.ftl
@@ -0,0 +1,7 @@
+cmd-playerpanel-desc = Displays general information and actions for a player
+cmd-playerpanel-help = Usage: playerpanel
+
+cmd-playerpanel-server = This command cannot be run from the server
+cmd-playerpanel-invalid-arguments = Invalid amount of arguments
+cmd-playerpanel-invalid-player = Player not found
+cmd-playerpanel-completion =