diff --git a/src/ReverseLlama.Server/AdminEndpoints.cs b/src/ReverseLlama.Server/AdminEndpoints.cs index 1333643..e9b5454 100644 --- a/src/ReverseLlama.Server/AdminEndpoints.cs +++ b/src/ReverseLlama.Server/AdminEndpoints.cs @@ -335,7 +335,9 @@ internal static class AdminEndpoints models = BuildModelSummaries(hub, store), apiKeys = store.ListApiKeys(), groups = store.ListGroups(), - apiKeyGroups = store.ListApiKeyGroups() + apiKeyGroups = store.ListApiKeyGroups(), + clientGroups = store.ResolveClientGroups( + hub.ClientSnapshots.Select(c => c.Id).ToList()) }; private static IReadOnlyList BuildClientSummaries(TunnelHub hub, ManagementStore store) diff --git a/src/ReverseLlama.Server/ManagementStore.cs b/src/ReverseLlama.Server/ManagementStore.cs index 5f5475c..eda7c84 100644 --- a/src/ReverseLlama.Server/ManagementStore.cs +++ b/src/ReverseLlama.Server/ManagementStore.cs @@ -934,6 +934,73 @@ internal sealed class ManagementStore } } + public IReadOnlyDictionary> ResolveClientGroups(IReadOnlyList clientIds) + { + if (!_isAvailable || clientIds.Count == 0) + { + return new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var clientId in clientIds) + { + result[clientId] = new SortedSet(StringComparer.OrdinalIgnoreCase); + } + + lock (_lock) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT g.name, gm.client_id, gm.client_pattern + FROM group_members gm + INNER JOIN groups g ON gm.group_id = g.id + """; + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var groupName = reader.GetString(0); + var explicitClientId = reader.IsDBNull(1) ? null : reader.GetString(1); + var pattern = reader.IsDBNull(2) ? null : reader.GetString(2); + + if (!string.IsNullOrWhiteSpace(explicitClientId) + && result.TryGetValue(explicitClientId, out var explicitGroups)) + { + explicitGroups.Add(groupName); + } + else if (!string.IsNullOrWhiteSpace(pattern)) + { + Regex? regex = null; + try + { + regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); + } + catch (RegexParseException) + { + continue; + } + + foreach (var clientId in clientIds) + { + if (regex.IsMatch(clientId) && result.TryGetValue(clientId, out var groups)) + { + groups.Add(groupName); + } + } + } + } + } + + var frozen = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var (clientId, groups) in result) + { + frozen[clientId] = groups.ToList(); + } + + return frozen; + } + private IReadOnlyList GetApiKeyGroupIdsLocked(string apiKeyId) { using var connection = OpenConnection(); diff --git a/src/ReverseLlama.Server/wwwroot/admin/app.css b/src/ReverseLlama.Server/wwwroot/admin/app.css index 33e8b67..6ebd7dc 100644 --- a/src/ReverseLlama.Server/wwwroot/admin/app.css +++ b/src/ReverseLlama.Server/wwwroot/admin/app.css @@ -281,7 +281,7 @@ textarea { table { width: 100%; border-collapse: collapse; - min-width: 760px; + min-width: 900px; } th, @@ -359,6 +359,17 @@ tr:last-child td { color: var(--amber); } +.badge[href] { + text-decoration: none; + cursor: pointer; +} + +.badge[href]:hover { + border-color: var(--blue); + background: #edf4ff; + color: var(--blue); +} + .metric-grid { display: grid; grid-template-columns: repeat(4, minmax(120px, 1fr)); diff --git a/src/ReverseLlama.Server/wwwroot/admin/app.js b/src/ReverseLlama.Server/wwwroot/admin/app.js index 3d5615f..9c8f41f 100644 --- a/src/ReverseLlama.Server/wwwroot/admin/app.js +++ b/src/ReverseLlama.Server/wwwroot/admin/app.js @@ -315,7 +315,10 @@ function renderClients() { } function clientsTable(clients) { - const rows = clients.map((client) => ` + const clientGroups = state.summary?.clientGroups || {}; + const rows = clients.map((client) => { + const groups = clientGroups[client.id] || []; + return `
${escapeHtml(client.id)}
@@ -335,6 +338,11 @@ function clientsTable(clients) { ${modelBadges(client.models)} ${modelBadges(client.activeModels)} + +
+ ${groups.length ? groups.map((g) => `${escapeHtml(g)}`).join("") : `None`} +
+
@@ -343,7 +351,7 @@ function clientsTable(clients) {
- `).join(""); + `}).join(""); return ` @@ -355,6 +363,7 @@ function clientsTable(clients) { +
Requests Listed models Active modelsGroups Actions