Polish role admin speech flow and permission tooltips
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.27 R289";
|
window.CHGRID_WEB_VERSION = "2026.02.27 R290";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -206,6 +206,11 @@ type AdminUserSummary = {
|
|||||||
status: 'active' | 'disabled';
|
status: 'active' | 'disabled';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AdminPendingUserMutation =
|
||||||
|
| { action: 'set_role'; username: string; role: string }
|
||||||
|
| { action: 'ban'; username: string }
|
||||||
|
| { action: 'unban'; username: string };
|
||||||
|
|
||||||
/** Builds linearized help-view lines from sectioned help content. */
|
/** Builds linearized help-view lines from sectioned help content. */
|
||||||
function buildHelpLines(help: HelpData): string[] {
|
function buildHelpLines(help: HelpData): string[] {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
@@ -303,6 +308,7 @@ let adminMenuIndex = 0;
|
|||||||
let adminRoles: AdminRoleSummary[] = [];
|
let adminRoles: AdminRoleSummary[] = [];
|
||||||
let adminRoleIndex = 0;
|
let adminRoleIndex = 0;
|
||||||
let adminPermissionKeys: string[] = [];
|
let adminPermissionKeys: string[] = [];
|
||||||
|
let adminPermissionTooltips: Record<string, string> = {};
|
||||||
let adminRolePermissionIndex = 0;
|
let adminRolePermissionIndex = 0;
|
||||||
let adminRoleDeleteReplacementIndex = 0;
|
let adminRoleDeleteReplacementIndex = 0;
|
||||||
let adminUsers: AdminUserSummary[] = [];
|
let adminUsers: AdminUserSummary[] = [];
|
||||||
@@ -310,7 +316,7 @@ let adminUserIndex = 0;
|
|||||||
let adminPendingUserAction: 'set_role' | 'ban' | 'unban' | null = null;
|
let adminPendingUserAction: 'set_role' | 'ban' | 'unban' | null = null;
|
||||||
let adminSelectedRoleName = '';
|
let adminSelectedRoleName = '';
|
||||||
let adminSelectedUsername = '';
|
let adminSelectedUsername = '';
|
||||||
let adminPendingRoleChange: { username: string; role: string } | null = null;
|
let adminPendingUserMutation: AdminPendingUserMutation | null = null;
|
||||||
let activeTeleport:
|
let activeTeleport:
|
||||||
| {
|
| {
|
||||||
startX: number;
|
startX: number;
|
||||||
@@ -1584,6 +1590,7 @@ function getAvailableAdminActions(): AdminMenuAction[] {
|
|||||||
function handleAdminRolesList(message: Extract<IncomingMessage, { type: 'admin_roles_list' }>): void {
|
function handleAdminRolesList(message: Extract<IncomingMessage, { type: 'admin_roles_list' }>): void {
|
||||||
adminRoles = [...message.roles].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
adminRoles = [...message.roles].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
||||||
adminPermissionKeys = [...message.permissionKeys].sort((a, b) => a.localeCompare(b));
|
adminPermissionKeys = [...message.permissionKeys].sort((a, b) => a.localeCompare(b));
|
||||||
|
adminPermissionTooltips = { ...(message.permissionTooltips ?? {}) };
|
||||||
if (adminPendingUserAction === 'set_role' && adminSelectedUsername) {
|
if (adminPendingUserAction === 'set_role' && adminSelectedUsername) {
|
||||||
state.mode = 'adminUserRoleSelect';
|
state.mode = 'adminUserRoleSelect';
|
||||||
adminRoleIndex = 0;
|
adminRoleIndex = 0;
|
||||||
@@ -1633,33 +1640,44 @@ function handleAdminActionResult(message: Extract<IncomingMessage, { type: 'admi
|
|||||||
if (message.action === 'role_update_permissions') {
|
if (message.action === 'role_update_permissions') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message.action === 'user_set_role') {
|
updateStatus(message.message);
|
||||||
if (message.ok && adminPendingRoleChange) {
|
if (!message.ok) {
|
||||||
for (const user of adminUsers) {
|
adminPendingUserMutation = null;
|
||||||
if (user.username === adminPendingRoleChange.username) {
|
|
||||||
user.role = adminPendingRoleChange.role;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.mode === 'adminUserList' && adminUsers.length > 0) {
|
|
||||||
adminUserIndex = Math.max(0, Math.min(adminUserIndex, adminUsers.length - 1));
|
|
||||||
const selected = adminUsers[adminUserIndex];
|
|
||||||
updateStatus(`${selected.username}, ${selected.role}, ${selected.status}.`);
|
|
||||||
}
|
|
||||||
adminPendingRoleChange = null;
|
|
||||||
audio.sfxUiConfirm();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
adminPendingRoleChange = null;
|
|
||||||
updateStatus(message.message);
|
|
||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateStatus(message.message);
|
|
||||||
if (message.ok) {
|
if (adminPendingUserMutation) {
|
||||||
audio.sfxUiConfirm();
|
if (adminPendingUserMutation.action === 'set_role') {
|
||||||
} else {
|
const target = adminUsers.find((entry) => entry.username === adminPendingUserMutation.username);
|
||||||
audio.sfxUiCancel();
|
if (target) {
|
||||||
|
target.role = adminPendingUserMutation.role;
|
||||||
|
}
|
||||||
|
} else if (adminPendingUserMutation.action === 'ban') {
|
||||||
|
adminUsers = adminUsers.filter((entry) => entry.username !== adminPendingUserMutation.username);
|
||||||
|
if (state.mode === 'adminUserList' && adminPendingUserAction === 'ban') {
|
||||||
|
if (adminUsers.length > 0) {
|
||||||
|
adminUserIndex = Math.max(0, Math.min(adminUserIndex, adminUsers.length - 1));
|
||||||
|
} else {
|
||||||
|
state.mode = 'adminMenu';
|
||||||
|
adminPendingUserAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (adminPendingUserMutation.action === 'unban') {
|
||||||
|
adminUsers = adminUsers.filter((entry) => entry.username !== adminPendingUserMutation.username);
|
||||||
|
if (state.mode === 'adminUserList' && adminPendingUserAction === 'unban') {
|
||||||
|
if (adminUsers.length > 0) {
|
||||||
|
adminUserIndex = Math.max(0, Math.min(adminUserIndex, adminUsers.length - 1));
|
||||||
|
} else {
|
||||||
|
state.mode = 'adminMenu';
|
||||||
|
adminPendingUserAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adminPendingUserMutation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio.sfxUiConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds dependencies shared by connect/disconnect flow helpers. */
|
/** Builds dependencies shared by connect/disconnect flow helpers. */
|
||||||
@@ -2741,7 +2759,7 @@ function handleAdminRolePermissionListModeInput(code: string, key: string): void
|
|||||||
}
|
}
|
||||||
const entries = [...adminPermissionKeys, '__delete_role__'];
|
const entries = [...adminPermissionKeys, '__delete_role__'];
|
||||||
const control = handleListControlKey(code, key, entries, adminRolePermissionIndex, (entry) =>
|
const control = handleListControlKey(code, key, entries, adminRolePermissionIndex, (entry) =>
|
||||||
entry === '__delete_role__' ? `Delete role ${role.name}` : `${entry} ${role.permissions.includes(entry) ? 'on' : 'off'}`,
|
entry === '__delete_role__' ? `Delete role ${role.name}` : `${entry}: ${role.permissions.includes(entry) ? 'on' : 'off'}`,
|
||||||
);
|
);
|
||||||
if (control.type === 'move') {
|
if (control.type === 'move') {
|
||||||
adminRolePermissionIndex = control.index;
|
adminRolePermissionIndex = control.index;
|
||||||
@@ -2749,7 +2767,17 @@ function handleAdminRolePermissionListModeInput(code: string, key: string): void
|
|||||||
if (value === '__delete_role__') {
|
if (value === '__delete_role__') {
|
||||||
updateStatus(`Delete role ${role.name}.`);
|
updateStatus(`Delete role ${role.name}.`);
|
||||||
} else {
|
} else {
|
||||||
updateStatus(`${value} ${role.permissions.includes(value) ? 'on' : 'off'}`);
|
updateStatus(`${value}: ${role.permissions.includes(value) ? 'on' : 'off'}`);
|
||||||
|
}
|
||||||
|
audio.sfxUiBlip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code === 'Space') {
|
||||||
|
const value = entries[adminRolePermissionIndex];
|
||||||
|
if (value === '__delete_role__') {
|
||||||
|
updateStatus('Delete the current role and reassign affected users.');
|
||||||
|
} else {
|
||||||
|
updateStatus(adminPermissionTooltips[value] || 'No tooltip available.');
|
||||||
}
|
}
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
@@ -2782,7 +2810,7 @@ function handleAdminRolePermissionListModeInput(code: string, key: string): void
|
|||||||
}
|
}
|
||||||
role.permissions = [...nextPermissions].sort((a, b) => a.localeCompare(b));
|
role.permissions = [...nextPermissions].sort((a, b) => a.localeCompare(b));
|
||||||
signaling.send({ type: 'admin_role_update_permissions', role: role.name, permissions: role.permissions });
|
signaling.send({ type: 'admin_role_update_permissions', role: role.name, permissions: role.permissions });
|
||||||
updateStatus(`${value} ${role.permissions.includes(value) ? 'on' : 'off'}`);
|
updateStatus(`${value}: ${role.permissions.includes(value) ? 'on' : 'off'}`);
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2803,7 +2831,7 @@ function handleAdminRoleDeleteReplacementModeInput(code: string, key: string): v
|
|||||||
const control = handleListControlKey(code, key, candidates, adminRoleDeleteReplacementIndex, (entry) => entry.name);
|
const control = handleListControlKey(code, key, candidates, adminRoleDeleteReplacementIndex, (entry) => entry.name);
|
||||||
if (control.type === 'move') {
|
if (control.type === 'move') {
|
||||||
adminRoleDeleteReplacementIndex = control.index;
|
adminRoleDeleteReplacementIndex = control.index;
|
||||||
updateStatus(`Replacement role: ${candidates[adminRoleDeleteReplacementIndex].name}.`);
|
updateStatus(candidates[adminRoleDeleteReplacementIndex].name);
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2850,29 +2878,13 @@ function handleAdminUserListModeInput(code: string, key: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (adminPendingUserAction === 'ban') {
|
if (adminPendingUserAction === 'ban') {
|
||||||
adminUsers.splice(adminUserIndex, 1);
|
adminPendingUserMutation = { action: 'ban', username: selected.username };
|
||||||
if (adminUsers.length > 0) {
|
|
||||||
adminUserIndex = Math.min(adminUserIndex, adminUsers.length - 1);
|
|
||||||
const next = adminUsers[adminUserIndex];
|
|
||||||
updateStatus(`${next.username}, ${next.role}, ${next.status}.`);
|
|
||||||
} else {
|
|
||||||
state.mode = 'adminMenu';
|
|
||||||
updateStatus('No users to ban.');
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'admin_user_ban', username: selected.username });
|
signaling.send({ type: 'admin_user_ban', username: selected.username });
|
||||||
adminPendingUserAction = 'ban';
|
adminPendingUserAction = 'ban';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (adminPendingUserAction === 'unban') {
|
if (adminPendingUserAction === 'unban') {
|
||||||
adminUsers.splice(adminUserIndex, 1);
|
adminPendingUserMutation = { action: 'unban', username: selected.username };
|
||||||
if (adminUsers.length > 0) {
|
|
||||||
adminUserIndex = Math.min(adminUserIndex, adminUsers.length - 1);
|
|
||||||
const next = adminUsers[adminUserIndex];
|
|
||||||
updateStatus(`${next.username}, ${next.role}, ${next.status}.`);
|
|
||||||
} else {
|
|
||||||
state.mode = 'adminMenu';
|
|
||||||
updateStatus('No users to unban.');
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'admin_user_unban', username: selected.username });
|
signaling.send({ type: 'admin_user_unban', username: selected.username });
|
||||||
adminPendingUserAction = 'unban';
|
adminPendingUserAction = 'unban';
|
||||||
return;
|
return;
|
||||||
@@ -2903,7 +2915,7 @@ function handleAdminUserRoleSelectModeInput(code: string, key: string): void {
|
|||||||
}
|
}
|
||||||
if (control.type === 'select') {
|
if (control.type === 'select') {
|
||||||
const selectedRole = adminRoles[adminRoleIndex];
|
const selectedRole = adminRoles[adminRoleIndex];
|
||||||
adminPendingRoleChange = { username: adminSelectedUsername, role: selectedRole.name };
|
adminPendingUserMutation = { action: 'set_role', username: adminSelectedUsername, role: selectedRole.name };
|
||||||
signaling.send({ type: 'admin_user_set_role', username: adminSelectedUsername, role: selectedRole.name });
|
signaling.send({ type: 'admin_user_set_role', username: adminSelectedUsername, role: selectedRole.name });
|
||||||
state.mode = 'adminUserList';
|
state.mode = 'adminUserList';
|
||||||
adminPendingUserAction = 'set_role';
|
adminPendingUserAction = 'set_role';
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ export const adminRolesListSchema = z.object({
|
|||||||
type: z.literal('admin_roles_list'),
|
type: z.literal('admin_roles_list'),
|
||||||
roles: z.array(adminRoleSummarySchema),
|
roles: z.array(adminRoleSummarySchema),
|
||||||
permissionKeys: z.array(z.string()),
|
permissionKeys: z.array(z.string()),
|
||||||
|
permissionTooltips: z.record(z.string(), z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const adminUsersListSchema = z.object({
|
export const adminUsersListSchema = z.object({
|
||||||
|
|||||||
@@ -46,6 +46,25 @@ PERMISSIONS: tuple[str, ...] = (
|
|||||||
"server.manage_settings",
|
"server.manage_settings",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PERMISSION_DESCRIPTIONS: dict[str, str] = {
|
||||||
|
"item.create": "Allow creating new items.",
|
||||||
|
"item.edit.own": "Allow editing items created by this user.",
|
||||||
|
"item.edit.any": "Allow editing any item.",
|
||||||
|
"item.delete.own": "Allow deleting items created by this user.",
|
||||||
|
"item.delete.any": "Allow deleting any item.",
|
||||||
|
"item.use": "Allow using item primary and secondary actions.",
|
||||||
|
"item.pickup_drop.own": "Allow picking up and dropping items created by this user.",
|
||||||
|
"item.pickup_drop.any": "Allow picking up and dropping any item.",
|
||||||
|
"chat.send": "Allow sending chat messages.",
|
||||||
|
"voice.send": "Allow transmitting microphone audio.",
|
||||||
|
"profile.update_nickname": "Allow changing nickname.",
|
||||||
|
"account.delete.any": "Allow deleting other user accounts.",
|
||||||
|
"user.ban_unban": "Allow banning and unbanning users.",
|
||||||
|
"user.change_role": "Allow assigning user roles.",
|
||||||
|
"role.manage": "Allow creating, editing, and deleting roles.",
|
||||||
|
"server.manage_settings": "Allow changing server settings.",
|
||||||
|
}
|
||||||
|
|
||||||
DEFAULT_ROLE_PERMISSIONS: dict[str, set[str]] = {
|
DEFAULT_ROLE_PERMISSIONS: dict[str, set[str]] = {
|
||||||
"admin": set(PERMISSIONS),
|
"admin": set(PERMISSIONS),
|
||||||
"editor": {
|
"editor": {
|
||||||
@@ -185,6 +204,11 @@ class AuthService:
|
|||||||
|
|
||||||
return list(PERMISSIONS)
|
return list(PERMISSIONS)
|
||||||
|
|
||||||
|
def list_all_permission_descriptions(self) -> dict[str, str]:
|
||||||
|
"""Return canonical permission tooltip text keyed by permission id."""
|
||||||
|
|
||||||
|
return {key: PERMISSION_DESCRIPTIONS.get(key, key) for key in PERMISSIONS}
|
||||||
|
|
||||||
def get_user_permissions(self, user_id: str) -> set[str]:
|
def get_user_permissions(self, user_id: str) -> set[str]:
|
||||||
"""Return current permission set for one user id."""
|
"""Return current permission set for one user id."""
|
||||||
|
|
||||||
@@ -829,7 +853,7 @@ class AuthService:
|
|||||||
|
|
||||||
now_ms = self.now_ms()
|
now_ms = self.now_ms()
|
||||||
for key in PERMISSIONS:
|
for key in PERMISSIONS:
|
||||||
description = f"Permission: {key}"
|
description = PERMISSION_DESCRIPTIONS.get(key, key)
|
||||||
self._db_execute(
|
self._db_execute(
|
||||||
"INSERT OR IGNORE INTO permissions (key, description) VALUES (?, ?)",
|
"INSERT OR IGNORE INTO permissions (key, description) VALUES (?, ?)",
|
||||||
(key, description),
|
(key, description),
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ class AdminRolesListResultPacket(BasePacket):
|
|||||||
type: Literal["admin_roles_list"]
|
type: Literal["admin_roles_list"]
|
||||||
roles: list[AdminRoleSummary]
|
roles: list[AdminRoleSummary]
|
||||||
permissionKeys: list[str]
|
permissionKeys: list[str]
|
||||||
|
permissionTooltips: dict[str, str] | None = None
|
||||||
|
|
||||||
|
|
||||||
class AdminUserSummary(BaseModel):
|
class AdminUserSummary(BaseModel):
|
||||||
|
|||||||
@@ -1607,6 +1607,7 @@ class SignalingServer:
|
|||||||
type="admin_roles_list",
|
type="admin_roles_list",
|
||||||
roles=roles,
|
roles=roles,
|
||||||
permissionKeys=self.auth_service.list_all_permissions(),
|
permissionKeys=self.auth_service.list_all_permissions(),
|
||||||
|
permissionTooltips=self.auth_service.list_all_permission_descriptions(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user