about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--MatrixRoomUtils.Core/AuthenticatedHomeServer.cs26
-rw-r--r--MatrixRoomUtils.Core/Interfaces/IHomeServer.cs17
-rw-r--r--MatrixRoomUtils.Core/RemoteHomeServer.cs3
-rw-r--r--MatrixRoomUtils.Core/Room.cs70
-rw-r--r--MatrixRoomUtils.Core/RuntimeCache.cs45
-rw-r--r--MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs16
-rw-r--r--MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj4
-rw-r--r--MatrixRoomUtils.Web/Pages/About.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/DevOptions.razor57
-rw-r--r--MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor7
-rw-r--r--MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor79
-rw-r--r--MatrixRoomUtils.Web/Pages/MediaLocator.razor119
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor3
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor13
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager.razor40
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor96
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor77
-rw-r--r--MatrixRoomUtils.Web/Properties/launchSettings.json4
-rw-r--r--MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor24
-rw-r--r--MatrixRoomUtils.Web/Shared/LogView.razor26
-rw-r--r--MatrixRoomUtils.Web/Shared/MainLayout.razor14
-rw-r--r--MatrixRoomUtils.Web/Shared/NavMenu.razor9
-rw-r--r--MatrixRoomUtils.Web/Shared/PortableDevTools.razor31
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListItem.razor33
-rwxr-xr-xdeploy.sh2
25 files changed, 680 insertions, 136 deletions
diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
index 6f5df39..7a1f5de 100644
--- a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
+++ b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
@@ -9,12 +9,14 @@ public class AuthenticatedHomeServer : IHomeServer
 {
     public string UserId { get; set; }
     public string AccessToken { get; set; }
+    public readonly HomeserverAdminApi Admin;
 
     public AuthenticatedHomeServer(string userId, string accessToken, string canonicalHomeServerDomain)
     {
         UserId = userId;
         AccessToken = accessToken;
         HomeServerDomain = canonicalHomeServerDomain;
+        Admin = new HomeserverAdminApi(this);
         _httpClient = new HttpClient();
     }
 
@@ -56,9 +58,25 @@ public class AuthenticatedHomeServer : IHomeServer
 
         return rooms;
     }
-
-    public async Task<string> ResolveMediaUri(string mxc)
+    
+    
+    
+    
+    
+    public class HomeserverAdminApi
     {
-        return mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/r0/download/");
+        private readonly AuthenticatedHomeServer _authenticatedHomeServer;
+
+        public HomeserverAdminApi(AuthenticatedHomeServer authenticatedHomeServer)
+        {
+            _authenticatedHomeServer = authenticatedHomeServer;
+        }
+    
+    
+        
+        
+        
+        
+        
     }
-}
\ No newline at end of file
+}
diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
index c6d788c..8fb8b2c 100644
--- a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
+++ b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
@@ -15,6 +15,18 @@ public class IHomeServer
 
     public async Task<string> ResolveHomeserverFromWellKnown(string homeserver)
     {
+        var res = await _resolveHomeserverFromWellKnown(homeserver);
+        if(!res.StartsWith("http")) res = "https://" + res;
+        if(res.EndsWith(":443")) res = res.Substring(0, res.Length - 4);
+        return res;
+    }
+    private async Task<string> _resolveHomeserverFromWellKnown(string homeserver)
+    {
+        if (RuntimeCache.HomeserverResolutionCache.Count == 0)
+        {
+            Console.WriteLine("No cached homeservers, resolving...");
+            await Task.Delay(Random.Shared.Next(1000, 5000));
+        } 
         if (RuntimeCache.HomeserverResolutionCache.ContainsKey(homeserver))
         {
             if (RuntimeCache.HomeserverResolutionCache[homeserver].ResolutionTime < DateTime.Now.AddHours(1))
@@ -22,6 +34,7 @@ public class IHomeServer
                 Console.WriteLine($"Found cached homeserver: {RuntimeCache.HomeserverResolutionCache[homeserver].Result}");
                 return RuntimeCache.HomeserverResolutionCache[homeserver].Result;
             }
+            Console.WriteLine($"Cached homeserver expired, removing: {RuntimeCache.HomeserverResolutionCache[homeserver].Result}");
             RuntimeCache.HomeserverResolutionCache.Remove(homeserver);
         }
         //throw new NotImplementedException();
@@ -95,4 +108,8 @@ public class IHomeServer
         _profileCache[mxid] = profile;
         return profile;
     }
+    public async Task<string> ResolveMediaUri(string mxc)
+    {
+        return mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/r0/download/");
+    }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/RemoteHomeServer.cs b/MatrixRoomUtils.Core/RemoteHomeServer.cs
index 9c096c8..942f873 100644
--- a/MatrixRoomUtils.Core/RemoteHomeServer.cs
+++ b/MatrixRoomUtils.Core/RemoteHomeServer.cs
@@ -1,7 +1,6 @@
 using System.Net.Http.Json;
 using System.Text.Json;
 using MatrixRoomUtils.Core.Interfaces;
-using MatrixRoomUtils.Core.Responses;
 
 namespace MatrixRoomUtils.Core;
 
@@ -13,12 +12,14 @@ public class RemoteHomeServer : IHomeServer
     {
         HomeServerDomain = canonicalHomeServerDomain;
         _httpClient = new HttpClient();
+        _httpClient.Timeout = TimeSpan.FromSeconds(5);
     }
     public async Task<RemoteHomeServer> Configure()
     {
         FullHomeServerDomain = await ResolveHomeserverFromWellKnown(HomeServerDomain);
         _httpClient.Dispose();
         _httpClient = new HttpClient { BaseAddress = new Uri(FullHomeServerDomain) };
+        _httpClient.Timeout = TimeSpan.FromSeconds(5);
         Console.WriteLine("[RHS] Finished setting up http client");
 
         return this;
diff --git a/MatrixRoomUtils.Core/Room.cs b/MatrixRoomUtils.Core/Room.cs
index 64db03d..6798d5b 100644
--- a/MatrixRoomUtils.Core/Room.cs
+++ b/MatrixRoomUtils.Core/Room.cs
@@ -1,10 +1,13 @@
 using System.Net.Http.Json;
 using System.Text.Json;
+using System.Web;
 
 namespace MatrixRoomUtils.Core;
 
 public class Room
 {
+    private static SemaphoreSlim _semaphore = new SemaphoreSlim(16, 16);
+
     private readonly HttpClient _httpClient;
     public string RoomId { get; set; }
 
@@ -13,31 +16,34 @@ public class Room
         _httpClient = httpClient;
         RoomId = roomId;
     }
-    
-    public async Task<JsonElement?> GetStateAsync(string type, string state_key="", bool logOnFailure = false)
+
+    public async Task<JsonElement?> GetStateAsync(string type, string state_key = "", bool logOnFailure = false)
     {
+        await _semaphore.WaitAsync();
         var url = $"/_matrix/client/v3/rooms/{RoomId}/state";
         if (!string.IsNullOrEmpty(state_key)) url += $"/{type}/{state_key}";
         else if (!string.IsNullOrEmpty(type)) url += $"/{type}";
-        var cache_key = "room_states_"+type;
+        var cache_key = "room_states:" + type;
         if (!RuntimeCache.GenericResponseCache.ContainsKey(cache_key))
         {
             Console.WriteLine($"[!!] No cache for {cache_key}, creating...");
-            RuntimeCache.GenericResponseCache.Add(cache_key, new ObjectCache<object?>()
-            {
-                DefaultExpiry = type switch
-                {
-                    "m.room.name" => TimeSpan.FromMinutes(15),
-                    _ => TimeSpan.FromMinutes(5)
-                }
-            });
+            RuntimeCache.GenericResponseCache.Add(cache_key, new ObjectCache<object?>());
         }
 
-        if (RuntimeCache.GenericResponseCache[cache_key][url] != null)
+        RuntimeCache.GenericResponseCache[cache_key].DefaultExpiry = type switch
+        {
+            "m.room.name" => TimeSpan.FromMinutes(30),
+            "org.matrix.mjolnir.shortcode" => TimeSpan.FromHours(4),
+            "" => TimeSpan.FromSeconds(0),
+            _ => TimeSpan.FromMinutes(15)
+        };
+
+        if (RuntimeCache.GenericResponseCache[cache_key].Cache.ContainsKey(url) && RuntimeCache.GenericResponseCache[cache_key][url] != null)
         {
-            if(RuntimeCache.GenericResponseCache[cache_key][url].ExpiryTime > DateTime.Now)
+            if (RuntimeCache.GenericResponseCache[cache_key][url].ExpiryTime > DateTime.Now)
             {
                 // Console.WriteLine($"[:3] Found cached state: {RuntimeCache.GenericResponseCache[cache_key][url].Result}");
+                _semaphore.Release();
                 return (JsonElement?)RuntimeCache.GenericResponseCache[cache_key][url].Result;
             }
             else
@@ -45,35 +51,55 @@ public class Room
                 Console.WriteLine($"[!!] Cached state expired at {RuntimeCache.GenericResponseCache[cache_key][url].ExpiryTime}: {RuntimeCache.GenericResponseCache[cache_key][url].Result}");
             }
         }
-        else
-        {
-            Console.WriteLine($"[!!] No cached state for {url}");
-        }
+        // else
+        // {
+        //     Console.WriteLine($"[!!] No cached state for {url}");
+        // }
 
         var res = await _httpClient.GetAsync(url);
         if (!res.IsSuccessStatusCode)
         {
-            if(logOnFailure) Console.WriteLine($"{RoomId}/{state_key}/{type} - got status: {res.StatusCode}");
+            if (logOnFailure) Console.WriteLine($"{RoomId}/{state_key}/{type} - got status: {res.StatusCode}");
+            _semaphore.Release();
             return null;
         }
+
         var result = await res.Content.ReadFromJsonAsync<JsonElement>();
+
+        if (!RuntimeCache.GenericResponseCache.ContainsKey(cache_key) && type != "")
+        {
+            Console.WriteLine($"[!!] No cache for {cache_key}, creating...");
+            RuntimeCache.GenericResponseCache.Add(cache_key, new ObjectCache<object?>());
+        }
+
         RuntimeCache.GenericResponseCache[cache_key][url] = new GenericResult<object>()
         {
             Result = result
         };
+        _semaphore.Release();
         return result;
     }
-    public async Task<string?> GetNameAsync()
+
+    public async Task<string> GetNameAsync()
     {
         var res = await GetStateAsync("m.room.name");
         if (!res.HasValue)
         {
             Console.WriteLine($"Room {RoomId} has no name!");
-            return null;
+            return RoomId;
         }
-        var resn = res?.TryGetProperty("name", out var name) ?? false ? name.GetString() : null;
+
+        var resn = res?.TryGetProperty("name", out var name) ?? false ? name.GetString() ?? RoomId : RoomId;
         //Console.WriteLine($"Got name: {resn}");
         return resn;
     }
-    
+
+    public async Task JoinAsync(string[]? homeservers = null)
+    {
+        string join_url = $"/_matrix/client/r0/join/{HttpUtility.UrlEncode(RoomId)}";
+        Console.WriteLine($"Calling {join_url} with {(homeservers == null ? 0 : homeservers.Length)} via's...");
+        if(homeservers == null || homeservers.Length == 0) homeservers = new[] { RoomId.Split(':')[1] };
+        var full_join_url = $"{join_url}?server_name=" + string.Join("&server_name=", homeservers);
+        var res = await _httpClient.PostAsync(full_join_url, null);
+    }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/RuntimeCache.cs b/MatrixRoomUtils.Core/RuntimeCache.cs
index affbd94..4a96f50 100644
--- a/MatrixRoomUtils.Core/RuntimeCache.cs
+++ b/MatrixRoomUtils.Core/RuntimeCache.cs
@@ -1,6 +1,3 @@
-using System.Runtime.InteropServices;
-using System.Runtime.InteropServices.JavaScript;
-using System.Xml.Schema;
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Responses;
 
@@ -40,17 +37,6 @@ public class ObjectCache<T> where T : class
     {
         get
         {
-            if (Random.Shared.Next(100) == 1)
-            {
-                // Console.WriteLine("Cleaning cache...");
-                // foreach (var x in Cache.Where(x => x.Value.ExpiryTime < DateTime.Now).OrderBy(x => x.Value.ExpiryTime).Take(3).ToList())
-                // {
-                    // Console.WriteLine($"Removing {x.Key} from cache");
-                    // Cache.Remove(x.Key);   
-                // }
-            }
-
-            
             if (Cache.ContainsKey(key))
             {
                 // Console.WriteLine($"Found item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}");
@@ -67,19 +53,48 @@ public class ObjectCache<T> where T : class
                     Console.WriteLine($"Failed to remove {key} from cache: {e.Message}");
                 }
             }
+            Console.WriteLine($"No item in cache: {key}");
             return null;
         }
         set
         {
             Cache[key] = value;
             if(Cache[key].ExpiryTime == null) Cache[key].ExpiryTime = DateTime.Now.Add(DefaultExpiry);
-            Console.WriteLine($"New item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}");
+            // Console.WriteLine($"New item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}");
             // Console.Error.WriteLine("Full cache: " + Cache.ToJson());
         }
     }
+    
+    public ObjectCache()
+    {
+        //expiry timer
+        Task.Run(async () =>
+        {
+            while (true)
+            {
+                await Task.Delay(1000);
+                foreach (var x in Cache.Where(x => x.Value.ExpiryTime < DateTime.Now).OrderBy(x => x.Value.ExpiryTime).Take(15).ToList())
+                {
+                    // Console.WriteLine($"Removing {x.Key} from cache");
+                    Cache.Remove(x.Key);   
+                }
+            }
+        });
+    }
 }
 public class GenericResult<T>
 {
     public T? Result { get; set; }
     public DateTime? ExpiryTime { get; set; }
+    
+    public GenericResult()
+    {
+        //expiry timer
+        
+    }
+    public GenericResult(T? result, DateTime? expiryTime = null) : this()
+    {
+        Result = result;
+        ExpiryTime = expiryTime;
+    }
 }
diff --git a/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs b/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs
index a439ea1..c224160 100644
--- a/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs
+++ b/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs
@@ -1,6 +1,5 @@
 using Blazored.LocalStorage;
 using MatrixRoomUtils.Core;
-using MatrixRoomUtils.Core.Extensions;
 
 namespace MatrixRoomUtils.Web.Classes;
 
@@ -27,7 +26,7 @@ public partial class LocalStorageWrapper
         {
             Console.WriteLine($"Access token is not null, creating authenticated home server");
             Console.WriteLine($"Homeserver cache: {RuntimeCache.HomeserverResolutionCache.Count} entries");
-            Console.WriteLine(RuntimeCache.HomeserverResolutionCache.ToJson());
+            // Console.WriteLine(RuntimeCache.HomeserverResolutionCache.ToJson());
             RuntimeCache.CurrentHomeServer = await new AuthenticatedHomeServer(RuntimeCache.LoginSessions[RuntimeCache.LastUsedToken].LoginResponse.UserId, RuntimeCache.LastUsedToken, RuntimeCache.LoginSessions[RuntimeCache.LastUsedToken].LoginResponse.HomeServer).Configure();
             Console.WriteLine("Created authenticated home server");
         }
@@ -47,6 +46,17 @@ public partial class LocalStorageWrapper
                 .ToDictionary(x => x.Key, x => x.Value));
         await localStorage.SetItemAsync("rory.matrixroomutils.generic_cache", RuntimeCache.GenericResponseCache);
     }
+    public static async Task SaveFieldToLocalStorage(ILocalStorageService localStorage, string key)
+    {
+        if (key == "rory.matrixroomutils.settings") await localStorage.SetItemAsync(key, Settings);
+        // if (key == "rory.matrixroomutils.token") await localStorage.SetItemAsStringAsync(key, RuntimeCache.AccessToken);
+        // if (key == "rory.matrixroomutils.current_homeserver") await localStorage.SetItemAsync(key, RuntimeCache.CurrentHomeserver);
+        if (key == "rory.matrixroomutils.user_cache") await localStorage.SetItemAsync(key, RuntimeCache.LoginSessions);
+        if (key == "rory.matrixroomutils.last_used_token") await localStorage.SetItemAsync(key, RuntimeCache.LastUsedToken);
+        if (key == "rory.matrixroomutils.homeserver_resolution_cache") await localStorage.SetItemAsync(key, RuntimeCache.HomeserverResolutionCache);
+        if (key == "rory.matrixroomutils.generic_cache") await localStorage.SetItemAsync(key, RuntimeCache.GenericResponseCache);
+        
+    }
 }
 
 
@@ -59,4 +69,6 @@ public class Settings
 public class DeveloperSettings
 {
     public bool EnableLogViewers { get; set; } = false;
+    public bool EnableConsoleLogging { get; set; } = true;
+    public bool EnablePortableDevtools { get; set; } = false;
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
index f167eaa..77a039c 100644
--- a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
+++ b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
@@ -20,4 +20,8 @@
       <_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
     </ItemGroup>
 
+    <ItemGroup>
+      <None Include="wwwroot\homeservers.txt" />
+    </ItemGroup>
+
 </Project>
diff --git a/MatrixRoomUtils.Web/Pages/About.razor b/MatrixRoomUtils.Web/Pages/About.razor
index fc9128e..d47e60b 100644
--- a/MatrixRoomUtils.Web/Pages/About.razor
+++ b/MatrixRoomUtils.Web/Pages/About.razor
@@ -1,5 +1,4 @@
 @page "/About"
-@using MatrixRoomUtils.Web.Shared.IndexComponents
 @using System.Net
 @inject NavigationManager NavigationManager
 @inject ILocalStorageService LocalStorage
diff --git a/MatrixRoomUtils.Web/Pages/DevOptions.razor b/MatrixRoomUtils.Web/Pages/DevOptions.razor
index 0cc38d8..e1b6ac0 100644
--- a/MatrixRoomUtils.Web/Pages/DevOptions.razor
+++ b/MatrixRoomUtils.Web/Pages/DevOptions.razor
@@ -1,6 +1,4 @@
 @page "/DevOptions"
-@using MatrixRoomUtils.Web.Shared.IndexComponents
-@using System.Net
 @using MatrixRoomUtils.Core.Extensions
 @inject NavigationManager NavigationManager
 @inject ILocalStorageService LocalStorage
@@ -10,8 +8,32 @@
 <h3>Rory&::MatrixUtils - Developer options</h3>
 <hr/>
 
-<InputCheckbox @bind-Value="@LocalStorageWrapper.Settings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox><label> Enable log views</label>
+<InputCheckbox @bind-Value="@LocalStorageWrapper.Settings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox><label> Enable log views</label><br/>
+<InputCheckbox @bind-Value="@LocalStorageWrapper.Settings.DeveloperSettings.EnableConsoleLogging" @oninput="@LogStuff"></InputCheckbox><label> Enable console logging</label><br/>
+<InputCheckbox @bind-Value="@LocalStorageWrapper.Settings.DeveloperSettings.EnablePortableDevtools" @oninput="@LogStuff"></InputCheckbox><label> Enable portable devtools</label><br/>
+<button @onclick="@DropCaches">Drop caches</button>
+<button @onclick="@RandomiseCacheTimers">Randomise cache timers</button>
+<br/>
 
+<details open>
+    <summary>View caches</summary>
+    <p>Generic cache:</p>
+    <ul>
+        @foreach (var item in RuntimeCache.GenericResponseCache)
+        {
+            <li>
+                @item.Key: @item.Value.Cache.Count entries<br/>
+                Default expiry: @item.Value.DefaultExpiry<br/>
+                @if (item.Value.Cache.Count > 0)
+                {
+                    <p>Earliest expiry: @(item.Value.Cache.Min(x => x.Value.ExpiryTime)) (@string.Format("{0:g}", item.Value.Cache.Min(x => x.Value.ExpiryTime).Value.Subtract(DateTime.Now)) from now)</p>
+                    @* <p>Average expiry: @(item.Value.Cache.Average(x => x.Value.ExpiryTime.Value))(@item.Value.Cache.Average(x => x.Value.ExpiryTime).Value.Subtract(DateTime.Now) from now)</p> *@
+                    <p>Last expiry: @(item.Value.Cache.Max(x => x.Value.ExpiryTime)) (@string.Format("{0:g}", item.Value.Cache.Max(x => x.Value.ExpiryTime).Value.Subtract(DateTime.Now)) from now)</p> 
+                }
+            </li>
+        }
+    </ul>
+</details>
 
 @code {
     protected override async Task OnInitializedAsync()
@@ -19,6 +41,14 @@
         await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         await base.OnInitializedAsync();
         await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
+        Task.Run(async () =>
+        {
+            while (true)
+            {
+                await Task.Delay(100);
+                StateHasChanged();
+            }
+        });
     }
 
     protected async Task LogStuff()
@@ -29,4 +59,25 @@
         await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
     }
 
+    protected async Task DropCaches()
+    {
+        RuntimeCache.GenericResponseCache.Clear();
+        RuntimeCache.HomeserverResolutionCache.Clear();
+        await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
+    }
+
+    protected async Task RandomiseCacheTimers()
+    {
+        foreach (var keyValuePair in RuntimeCache.GenericResponseCache)
+        {
+            Console.WriteLine($"Randomising cache timer for {keyValuePair.Key}");
+            foreach (var cacheItem in keyValuePair.Value.Cache)
+            {
+                cacheItem.Value.ExpiryTime = DateTime.Now.AddSeconds(Random.Shared.Next(15, 120));
+            }
+            
+            await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor
new file mode 100644
index 0000000..b77012b
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -0,0 +1,7 @@
+@page "/HSAdmin"
+<h3>Homeserver Admininistration</h3>
+<hr/>
+
+@code {
+    
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor b/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
new file mode 100644
index 0000000..f396025
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
@@ -0,0 +1,79 @@
+@page "/KnownHomeserverList"
+@using System.Text.Json
+@using MatrixRoomUtils.Core.Extensions
+<h3>Known Homeserver List</h3>
+<hr/>
+
+@if (!IsFinished)
+{
+    <p>Loading... Please wait...</p>
+}
+else
+{
+    @foreach (var server in HomeServers.OrderByDescending(x => x.KnownUserCount).ThenBy(x => x.Server).ToList())
+    {
+        <p>@server.Server - @server.KnownUserCount</p>
+    }
+}
+<hr/>
+
+@code {
+    List<HomeServerInfo> HomeServers = new();
+    bool IsFinished { get; set; }
+
+    protected override async Task OnInitializedAsync()
+    {
+        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+
+        HomeServers = await GetHomeservers();
+
+        IsFinished = true;
+        StateHasChanged();
+        Console.WriteLine("Rerendered!");
+        await base.OnInitializedAsync();
+    }
+
+
+    private async Task<List<HomeServerInfo>> GetHomeservers()
+    {
+        List<HomeServerInfo> homeServers = new();
+        var rooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms();
+        // Dictionary<string, StateEvent> roomMembers = new();
+        //start a task for each room
+        var tasks = rooms.Select(async room =>
+        {
+            Console.WriteLine($"Fetching states for room ({rooms.IndexOf(room)}/{rooms.Count}) ({room.RoomId})");
+            StateHasChanged();
+
+            var states = (await room.GetStateAsync("")).Value.Deserialize<List<StateEvent>>();
+            states.RemoveAll(x => x.type != "m.room.member");
+            Console.WriteLine($"Room {room.RoomId} has {states.Count} members");
+            foreach (var state in states)
+            {
+                if (!homeServers.Any(x => x.Server == state.state_key.Split(':')[1]))
+                {
+                    homeServers.Add(new HomeServerInfo() { Server = state.state_key.Split(':')[1] });
+                }
+                var hs = homeServers.First(x => x.Server == state.state_key.Split(':')[1]);
+                if(!hs.KnownUsers.Contains(state.state_key.Split(':')[0]))
+                    hs.KnownUsers.Add(state.state_key.Split(':')[0]);
+            }
+            Console.WriteLine("Collected states!");
+        });
+        await Task.WhenAll(tasks);
+        
+        Console.WriteLine("Calculating member counts...");
+        homeServers.ForEach(x => x.KnownUserCount = x.KnownUsers.Count);
+        Console.WriteLine(homeServers.First(x=>x.Server=="rory.gay").ToJson());
+        Console.WriteLine("Recalculated!");
+        return homeServers;
+    }
+
+    class HomeServerInfo
+    {
+        public string Server { get; set; }
+        public int? KnownUserCount { get; set; }
+        public List<string> KnownUsers { get; set; } = new();
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/MediaLocator.razor b/MatrixRoomUtils.Web/Pages/MediaLocator.razor
index cd244ef..06256f0 100644
--- a/MatrixRoomUtils.Web/Pages/MediaLocator.razor
+++ b/MatrixRoomUtils.Web/Pages/MediaLocator.razor
@@ -1,11 +1,46 @@
 @page "/MediaLocator"
-<h3>MediaLocator</h3>
+@inject HttpClient Http
+<h3>Media locator</h3>
 <hr/>
 
+<b>This is going to expose your IP address to all these homeservers!</b>
+<details>
+    <summary>Checked homeserver list (@homeservers.Count entries)</summary>
+    <ul>
+        @foreach (var hs in homeservers)
+        {
+            <li>@hs</li>
+        }
+    </ul>
+</details>
+<button @onclick="addMoreHomeservers">Add more homeservers</button>
+<br/>
 <span>MXC URL: </span>
 <input type="text" @bind="mxcUrl" />
 <button @onclick="executeSearch">Search</button>
 
+@if (successResults.Count > 0)
+{
+    <h4>Successes</h4>
+    <ul>
+        @foreach (var result in successResults)
+        {
+            <li>@result</li>
+        }
+    </ul>
+}
+
+@if (errorResults.Count > 0)
+{
+    <h4>Errors</h4>
+    <ul>
+        @foreach (var result in errorResults)
+        {
+            <li>@result</li>
+        }
+    </ul>
+}
+
 
 @code {
     string mxcUrl { get; set; }
@@ -15,22 +50,82 @@
 
     protected override async Task OnInitializedAsync()
     {
-        base.OnInitializedAsync();
-        
+        await base.OnInitializedAsync();
+        homeservers.AddRange(new []
+        {
+            "matrix.org",
+            "feline.support",
+            "rory.gay",
+            "the-apothecary.club",
+            "envs.net",
+            "projectsegfau.lt"
+        });
     }
 
     async Task executeSearch()
     {
-        var client = new HttpClient();
-        var response = await client.GetAsync($"https://matrix.org/_matrix/media/r0/identicon/{mxcUrl}");
-        if (response.IsSuccessStatusCode)
+        var sem = new SemaphoreSlim(128, 128);
+        homeservers.ForEach(async hs =>
         {
-            successResults.Add(mxcUrl);
-        }
-        else
-        {
-            errorResults.Add(mxcUrl);
-        }
+            await sem.WaitAsync();
+            var httpClient = new HttpClient { BaseAddress = new Uri(hs) };
+            httpClient.Timeout = TimeSpan.FromSeconds(5);
+            var rmu = mxcUrl.Replace("mxc://", $"{hs}/_matrix/media/r0/download/");
+            try
+            {
+                var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, rmu));
+                if (res.IsSuccessStatusCode)
+                {
+                    successResults.Add($"{hs}: found - {res.Content.Headers.ContentLength} bytes");
+                    StateHasChanged();
+                    return;
+                }
+                errorResults.Add($"Error: {hs} - {res.StatusCode}\n" + await res.Content.ReadAsStringAsync());
+            }
+            catch (Exception e)
+            {
+                errorResults.Add($"Error: {e}");
+            }
+            finally
+            {
+                sem.Release();
+            }
+            StateHasChanged();
+        });
     }
 
+
+    async Task addMoreHomeservers()
+    {
+        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+        var res = await Http.GetAsync("/homeservers.txt");
+        var content = await res.Content.ReadAsStringAsync();
+        homeservers.Clear();
+        var lines = content.Split("\n");
+        
+        var rhs = new RemoteHomeServer("rory.gay");
+        var sem = new SemaphoreSlim(128, 128);
+        lines.ToList().ForEach(async line =>
+        {
+            await sem.WaitAsync();
+            try
+            {
+                homeservers.Add(await rhs.ResolveHomeserverFromWellKnown(line));
+                StateHasChanged();
+                if(Random.Shared.Next(0,101) == 50) 
+                    await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e);
+            }
+            finally
+            {
+                sem.Release();
+            }
+        });
+
+
+        StateHasChanged();
+    }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
index 5dfb2d6..d0f9b87 100644
--- a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
+++ b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
@@ -41,7 +41,8 @@ else
                     @policyEvent.content.ExpiryDateTime
                 </td>
                 <td>
-                    <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
+                    <button class="btn" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Edit</button>
+                    @* <button class="btn btn-danger" $1$ @onclick="async () => await RemovePolicyAsync(policyEvent)" #1#>Remove</button> *@
                 </td>
             </tr>
         }
diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
index e9d1be4..f25fbae 100644
--- a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
+++ b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
@@ -19,8 +19,13 @@ else
     }
     foreach (var s in PolicyRoomList)
     {
-        <a href="@(NavigationManager.Uri + "/" + s.RoomId.Replace('.', '~'))">[@s.Shortcode] @s.Name (@s.RoomId)</a>
-        <br/>
+        
+        <a style="color: unset; text-decoration: unset;" href="/PolicyListEditor/@s.RoomId.Replace('.','~')"><RoomListItem RoomId="@s.RoomId">
+            <br/>
+            <span>Shortcode: @s.Shortcode</span>
+        </RoomListItem></a>
+        @* <a href="@(NavigationManager.Uri + "/" + s.RoomId.Replace('.', '~'))">[@s.Shortcode] @s.Name (@s.RoomId)</a> *@
+        @* <br/> *@
     }
 }
 
@@ -74,15 +79,11 @@ else
     {
         try
         {
-    //TODO: refactor!!!!!
             await semaphore.WaitAsync();
             PolicyRoomInfo roomInfo = new()
             {
                 RoomId = room
             };
-
-
-    // --- //
             var r = await RuntimeCache.CurrentHomeServer.GetRoom(room);
             var shortcodeState = await r.GetStateAsync("org.matrix.mjolnir.shortcode");
             if(!shortcodeState.HasValue) return null;
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager.razor b/MatrixRoomUtils.Web/Pages/RoomManager.razor
deleted file mode 100644
index deb6fd5..0000000
--- a/MatrixRoomUtils.Web/Pages/RoomManager.razor
+++ /dev/null
@@ -1,40 +0,0 @@
-@page "/RoomManager"
-@inject ILocalStorageService LocalStorage
-@inject NavigationManager NavigationManager
-<h3>Room manager</h3>
-<hr/>
-@if (Rooms.Count == 0)
-{
-    <p>You are not in any rooms!</p>
-    @* <p>Loading progress: @checkedRoomCount/@totalRoomCount</p> *@
-}
-else
-{
-    <details open>
-        <summary>Room List</summary>
-        @foreach (var room in Rooms)
-        {
-            <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@room.Replace('.', '~')"><RoomListItem RoomId="@room" ShowOwnProfile="true"></RoomListItem></a>
-        }
-    </details>
-    
-}
-
-<div style="margin-bottom: 4em;"></div>
-<LogView></LogView>
-
-@code {
-    public List<string> Rooms { get; set; } = new();
-    protected override async Task OnInitializedAsync()
-    {
-        if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-        await base.OnInitializedAsync();
-        if (RuntimeCache.CurrentHomeServer == null)
-        {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
-        Rooms = (await RuntimeCache.CurrentHomeServer.GetJoinedRooms()).Select(x=>x.RoomId).ToList();
-        Console.WriteLine("Fetched joined rooms!");
-    }
-} 
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor
new file mode 100644
index 0000000..6d27679
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor
@@ -0,0 +1,96 @@
+@page "/RoomManager"
+@inject ILocalStorageService LocalStorage
+@inject NavigationManager NavigationManager
+<h3>Room manager</h3>
+<hr/>
+@if (Rooms.Count == 0)
+{
+    <p>You are not in any rooms!</p>
+    @* <p>Loading progress: @checkedRoomCount/@totalRoomCount</p> *@
+}
+else
+{
+    <p>You are in @Rooms.Count rooms and @Spaces.Count spaces</p>
+    <details open>
+        <summary>Space List</summary>
+        @foreach (var room in Spaces)
+        {
+            <a style="color: unset; text-decoration: unset;" href="/RoomManager/Space/@room.RoomId.Replace('.', '~')"><RoomListItem Room="@room" ShowOwnProfile="true"></RoomListItem></a>
+        }
+    </details>
+    <details open>
+        <summary>Room List</summary>
+        @foreach (var room in Rooms)
+        {
+            <a style="color: unset; text-decoration: unset;" href="/RoomManager/Room/@room.RoomId.Replace('.', '~')"><RoomListItem Room="@room" ShowOwnProfile="true"></RoomListItem></a>
+        }
+    </details>
+    
+}
+
+<div style="margin-bottom: 4em;"></div>
+<LogView></LogView>
+
+@code {
+    public List<Room> Rooms { get; set; } = new();
+    public List<Room> Spaces { get; set; } = new();
+    protected override async Task OnInitializedAsync()
+    {
+        if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+        await base.OnInitializedAsync();
+        if (RuntimeCache.CurrentHomeServer == null)
+        {
+            NavigationManager.NavigateTo("/Login");
+            return;
+        }
+        Rooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms();
+        StateHasChanged();
+        var semaphore = new SemaphoreSlim(1000);
+        var tasks = new List<Task<Room?>>();
+        foreach (var room in Rooms)
+        {
+            tasks.Add(CheckIfSpace(room, semaphore));
+        }
+        await Task.WhenAll(tasks);
+        
+        Console.WriteLine("Fetched joined rooms!");
+    }
+    
+    private async Task<Room?> CheckIfSpace(Room room, SemaphoreSlim semaphore)
+    {
+        await semaphore.WaitAsync();
+        try
+        {
+            var state = await room.GetStateAsync("m.room.create");
+            if (state != null)
+            {
+                //Console.WriteLine(state.Value.ToJson());
+                if(state.Value.TryGetProperty("type", out var type))
+                {
+                    if(type.ToString() == "m.space")
+                    {
+                        Spaces.Add(room);
+                        Rooms.Remove(room);
+                        StateHasChanged();
+                        return room;
+                    }
+                }
+                else
+                {
+                    //this is fine, apprently...
+                    //Console.WriteLine($"Room {room.RoomId} has no content.type in m.room.create!");
+                }
+            }
+        }
+        catch (Exception e)
+        {
+            Console.WriteLine(e);
+            return null;
+        }
+        finally
+        {
+            semaphore.Release();
+        }
+        return null;
+    }
+} 
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
new file mode 100644
index 0000000..4a5bddf
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
@@ -0,0 +1,77 @@
+@page "/RoomManager/Space/{RoomId}"
+@using MatrixRoomUtils.Core.Extensions
+@using System.Text.Json
+<h3>Room manager - Viewing Space</h3>
+
+<button onclick="@JoinAllRooms">Join all rooms</button>
+@foreach (var room in Rooms)
+{
+    <RoomListItem Room="room" ShowOwnProfile="true"></RoomListItem>
+}
+
+
+<br/>
+<details style="background: #0002;">
+    <summary style="background: #fff1;">State list</summary>
+    @foreach (var stateEvent in States.OrderBy(x => x.state_key).ThenBy(x => x.type))
+    {
+        <p>@stateEvent.state_key/@stateEvent.type:</p>
+        <pre>@stateEvent.content.ToJson()</pre>
+    }
+</details>
+
+@code {
+
+    [Parameter]
+    public string RoomId { get; set; } = "invalid!!!!!!";
+    
+    private Room? Room { get; set; }
+    
+    private StateEvent<object>[] States { get; set; } = Array.Empty<StateEvent<object>>();
+    private List<Room> Rooms { get; set; } = new();
+    
+    protected override async Task OnInitializedAsync()
+    {
+        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+        Room = await RuntimeCache.CurrentHomeServer.GetRoom(RoomId.Replace('~', '.'));
+        var state = await Room.GetStateAsync("");
+        if (state != null)
+        {
+            Console.WriteLine(state.Value.ToJson());
+            States = state.Value.Deserialize<StateEvent<object>[]>()!;
+            
+            foreach (var stateEvent in States)
+            {
+                if (stateEvent.type == "m.space.child")
+                {
+                    // if (stateEvent.content.ToJson().Length < 5) return;
+                    var roomId = stateEvent.state_key;
+                    var room = await RuntimeCache.CurrentHomeServer.GetRoom(roomId);
+                    if (room != null)
+                    {
+                        Rooms.Add(room);
+                    }
+                }
+            }
+            
+        // if(state.Value.TryGetProperty("type", out var type))
+        // {
+        // }
+        // else
+        // {
+        //     //this is fine, apprently...
+        //     //Console.WriteLine($"Room {room.RoomId} has no content.type in m.room.create!");
+        // }
+        }
+        await base.OnInitializedAsync();
+    }
+    
+    private async Task JoinAllRooms()
+    {
+        foreach (var room in Rooms)
+        {
+            room.JoinAsync();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Properties/launchSettings.json b/MatrixRoomUtils.Web/Properties/launchSettings.json
index d5a26f5..aa41dc8 100644
--- a/MatrixRoomUtils.Web/Properties/launchSettings.json
+++ b/MatrixRoomUtils.Web/Properties/launchSettings.json
@@ -11,7 +11,7 @@
     "http": {
       "commandName": "Project",
       "dotnetRunMessages": true,
-      "launchBrowser": true,
+      "launchBrowser": false,
       "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
       "applicationUrl": "http://localhost:5117",
       "environmentVariables": {
@@ -21,7 +21,7 @@
     "https": {
       "commandName": "Project",
       "dotnetRunMessages": true,
-      "launchBrowser": true,
+      "launchBrowser": false,
       "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
       "applicationUrl": "https://localhost:7014;http://localhost:5117",
       "environmentVariables": {
diff --git a/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor b/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
index 08161b2..87ef831 100644
--- a/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
+++ b/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
@@ -10,22 +10,30 @@
 
 <div style="margin-bottom: 1em;">
     <img style="border-radius: 50%; height: 3em; width: 3em;" src="@_avatarUrl"/>
-    <span style="margin-left: 1em;"><input type="radio" name="csa" checked="@(RuntimeCache.LastUsedToken == User.AccessToken)" onclick="@SetCurrent" style="text-decoration-line: unset;"/> <b>@User.Profile.DisplayName</b> on <b>@User.LoginResponse.HomeServer</b></span>
-    <a href="#" onclick="@RemoveUser">Remove</a>
+    <p style="margin-left: 1em; margin-top: -0.5em; display: inline-block;">
+        <input type="radio" name="csa" checked="@(RuntimeCache.LastUsedToken == User.AccessToken)" onclick="@SetCurrent" style="text-decoration-line: unset;"/>
+        <b>@User.Profile.DisplayName</b> on <b>@User.LoginResponse.HomeServer</b>
+        <a href="#" onclick="@RemoveUser">Remove</a>
+    </p>
+    <p style="margin-top: -1.5em; margin-left: 4em;">Member of @_roomCount rooms</p>
+
 </div>
 
 @code {
 
     [Parameter]
     public UserInfo User { get; set; } = null!;
-    
+
     private string _avatarUrl { get; set; }
+    private int _roomCount { get; set; } = 0;
 
     protected override async Task OnInitializedAsync()
     {
+        var hs = await new AuthenticatedHomeServer(User.LoginResponse.UserId, User.AccessToken, User.LoginResponse.HomeServer).Configure();
         if (User.Profile.AvatarUrl != null && User.Profile.AvatarUrl != "")
-            _avatarUrl = await (await new AuthenticatedHomeServer(User.LoginResponse.UserId, User.AccessToken, User.LoginResponse.HomeServer).Configure()).ResolveMediaUri(User.Profile.AvatarUrl);
+            _avatarUrl = await hs.ResolveMediaUri(User.Profile.AvatarUrl);
         else _avatarUrl = "https://api.dicebear.com/6.x/identicon/svg?seed=" + User.LoginResponse.UserId;
+        _roomCount = (await hs.GetJoinedRooms()).Count;
         await base.OnInitializedAsync();
     }
 
@@ -34,15 +42,17 @@
         Console.WriteLine(User.ToJson());
         RuntimeCache.LoginSessions.Remove(User.AccessToken);
         await LocalStorageWrapper.ReloadLocalStorage(LocalStorage);
-        
+
         StateHasChanged();
     }
+
     private async Task SetCurrent()
     {
         RuntimeCache.LastUsedToken = User.AccessToken;
-        //RuntimeCache.CurrentHomeserver = await MatrixAuth.ResolveHomeserverFromWellKnown(LocalStorageWrapper.LoginSessions[Token].LoginResponse.HomeServer);
+    //RuntimeCache.CurrentHomeserver = await MatrixAuth.ResolveHomeserverFromWellKnown(LocalStorageWrapper.LoginSessions[Token].LoginResponse.HomeServer);
         await LocalStorageWrapper.ReloadLocalStorage(LocalStorage);
-        
+
         StateHasChanged();
     }
+
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/LogView.razor b/MatrixRoomUtils.Web/Shared/LogView.razor
index f60f271..80fd355 100644
--- a/MatrixRoomUtils.Web/Shared/LogView.razor
+++ b/MatrixRoomUtils.Web/Shared/LogView.razor
@@ -1,13 +1,27 @@
 @using System.Text
-<u>Logs</u><br/>
-<pre>
-    @_stringBuilder
-</pre>
+@if (LocalStorageWrapper.Settings.DeveloperSettings.EnableLogViewers)
+{
+    <u>Logs</u>
+    <br/>
+    <pre>
+        @_stringBuilder
+    </pre>
+}
 
 @code {
     StringBuilder _stringBuilder = new();
-    protected override void OnInitialized()
+    protected override async Task OnInitializedAsync()
     {
+        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+        if (!LocalStorageWrapper.Settings.DeveloperSettings.EnableConsoleLogging)
+        {
+            Console.WriteLine("Console logging disabled!");
+            var _sw = new StringWriter();
+            Console.SetOut(_sw);
+            Console.SetError(_sw);
+            return;
+        }
+        if (!LocalStorageWrapper.Settings.DeveloperSettings.EnableLogViewers) return;
         //intecept stdout with textwriter to get logs
         var sw = new StringWriter(_stringBuilder);
         Console.SetOut(sw);
@@ -27,6 +41,6 @@
             }
     // ReSharper disable once FunctionNeverReturns - This is intentional behavior
         });
-        base.OnInitialized();
+        await base.OnInitializedAsync();
     }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/MainLayout.razor b/MatrixRoomUtils.Web/Shared/MainLayout.razor
index da27978..b1b0b53 100644
--- a/MatrixRoomUtils.Web/Shared/MainLayout.razor
+++ b/MatrixRoomUtils.Web/Shared/MainLayout.razor
@@ -1,5 +1,4 @@
-@using MatrixRoomUtils.Core.Extensions
-@using System.Net
+@using System.Net
 @inherits LayoutComponentBase
 
 <div class="page">
@@ -9,6 +8,7 @@
 
     <main>
         <div class="top-row px-4">
+            <PortableDevTools></PortableDevTools>
             <a href="https://git.rory.gay/MatrixRoomUtils.git/" target="_blank">Git</a>
             <a href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support" target="_blank">Matrix</a>
             @if (showDownload)
@@ -31,6 +31,16 @@
         using var hc = new HttpClient();
         var hr = await hc.SendAsync(new(HttpMethod.Head, NavigationManager.ToAbsoluteUri("/MRU-BIN.tar.xz").AbsoluteUri));
         showDownload = hr.StatusCode == HttpStatusCode.OK;
+
+        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+        if (!LocalStorageWrapper.Settings.DeveloperSettings.EnableConsoleLogging)
+        {
+            Console.WriteLine("Console logging disabled!");
+            var sw = new StringWriter();
+            Console.SetOut(sw);
+            Console.SetError(sw);
+        }
+        
         await base.OnInitializedAsync();
 
     }
diff --git a/MatrixRoomUtils.Web/Shared/NavMenu.razor b/MatrixRoomUtils.Web/Shared/NavMenu.razor
index b77935d..8136715 100644
--- a/MatrixRoomUtils.Web/Shared/NavMenu.razor
+++ b/MatrixRoomUtils.Web/Shared/NavMenu.razor
@@ -52,10 +52,15 @@
             <hr style="margin-bottom: 0em;"/>
         </div>
         <div class="nav-item px-3">
-            <NavLink class="nav-link" href="MediaLocator">
-                <span class="oi oi-plus" aria-hidden="true"></span> Media locator
+            <NavLink class="nav-link" href="KnownHomeserverList">
+                <span class="oi oi-plus" aria-hidden="true"></span> Known homeserver list
             </NavLink>
         </div>
+        @* <div class="nav-item px-3"> *@
+        @*     <NavLink class="nav-link" href="MediaLocator"> *@
+        @*         <span class="oi oi-plus" aria-hidden="true"></span> Media locator *@
+        @*     </NavLink> *@
+        @* </div> *@
         <div class="nav-item px-3">
             <h5 style="margin-left: 1em;">MRU</h5>
             <hr style="margin-bottom: 0em;"/>
diff --git a/MatrixRoomUtils.Web/Shared/PortableDevTools.razor b/MatrixRoomUtils.Web/Shared/PortableDevTools.razor
new file mode 100644
index 0000000..84e7791
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/PortableDevTools.razor
@@ -0,0 +1,31 @@
+
+@if (Enabled)
+{
+    <a href="/DevOptions">Portable devtools (enabled)</a>
+    <div id="PortableDevTools" style="position: fixed; bottom: 0; right: 0; min-width: 200px; min-height: 100px; background: #0002;" draggable>
+        <p>Cache size: @RuntimeCache.GenericResponseCache.Sum(x=>x.Value.Cache.Count)</p>
+    </div>
+}
+else {
+    <a href="/DevOptions">Portable devtools (disabled)</a>
+}
+
+@code {
+    private bool Enabled { get; set; } = LocalStorageWrapper.Settings.DeveloperSettings.EnablePortableDevtools;
+
+    protected override async Task OnInitializedAsync()
+    {
+        // if(!RuntimeCache.WasLoaded)
+            // await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+        // StateHasChanged();
+        Task.Run(async () =>
+        {
+            while (true)
+            {
+                await Task.Delay(100);
+                Enabled = LocalStorageWrapper.Settings.DeveloperSettings.EnablePortableDevtools;
+                StateHasChanged();
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
index 15ca5c0..16ced75 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListItem.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
@@ -3,17 +3,27 @@
 <div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-content;">
     @if (ShowOwnProfile)
     {
-        <img style="width: 32px; height:  32px; border-radius: 50%; @(hasCustomProfileAvatar ? "border-color: red; border-width: 3px; border-style: dashed;" : "")" src="@profileAvatar"/>
+        <img style="@(ChildContent != null ? "vertical-align: baseline;":"") width: 32px; height:  32px; border-radius: 50%; @(hasCustomProfileAvatar ? "border-color: red; border-width: 3px; border-style: dashed;" : "")" src="@profileAvatar"/>
         <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px; @(hasCustomProfileName ? "background-color: red;" : "")">@profileName</span>
         <span style="vertical-align: middle; padding-right: 8px; padding-left: 0px;">-></span>
     }
-    <img style="width: 32px; height:  32px; border-radius: 50%;" src="@roomIcon"/>
-    <span style="vertical-align: middle; padding-right: 8px;">@roomName</span>
+    <img style="@(ChildContent != null ? "vertical-align: baseline;":"") width: 32px; height:  32px; border-radius: 50%;" src="@roomIcon"/>
+    <div style="display: inline-block;">
+        <span style="vertical-align: middle; padding-right: 8px;">@roomName</span>
+        @if (ChildContent != null)
+        {
+            @ChildContent
+        }
+    </div>
+
 </div>
 
 @code {
 
     [Parameter]
+    public RenderFragment? ChildContent { get; set; }
+
+    [Parameter]
     public Room Room { get; set; }
 
     [Parameter]
@@ -33,8 +43,9 @@
     protected override async Task OnInitializedAsync()
     {
         await base.OnInitializedAsync();
-        
-        if(!RuntimeCache.WasLoaded) {
+
+        if (!RuntimeCache.WasLoaded)
+        {
             Console.WriteLine("Loading from local storage");
             await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         }
@@ -47,6 +58,10 @@
             }
             Room = await RuntimeCache.CurrentHomeServer.GetRoom(RoomId);
         }
+        else
+        {
+            RoomId = Room.RoomId;
+        }
 
         roomName = await Room.GetNameAsync();
         if (roomName == null)
@@ -66,8 +81,8 @@
 
         if (ShowOwnProfile)
         {
-            var profile = await RuntimeCache.CurrentHomeServer.GetProfile(RuntimeCache.CurrentHomeServer.UserId, debounce: true); 
-                
+            var profile = await RuntimeCache.CurrentHomeServer.GetProfile(RuntimeCache.CurrentHomeServer.UserId, debounce: true);
+
             var memberState = await Room.GetStateAsync("m.room.member", RuntimeCache.CurrentHomeServer.UserId);
             if (memberState.HasValue)
             {
@@ -86,7 +101,7 @@
                 {
                     hasCustomProfileName = _name.GetString() != profile.DisplayName;
                     profileName = _name.GetString();
-                    // Console.WriteLine($"{profile.DisplayName} - {_name.GetString()}: {hasCustomProfileName}");
+    // Console.WriteLine($"{profile.DisplayName} - {_name.GetString()}: {hasCustomProfileName}");
                 }
                 else
                 {
@@ -94,7 +109,7 @@
                 }
             }
         }
-        if(Random.Shared.Next(100) == 1)
+        if (Random.Shared.Next(100) == 1)
             await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
     }
 
diff --git a/deploy.sh b/deploy.sh
index d737787..9762db5 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -6,7 +6,7 @@ dotnet publish -c Release
 rsync -raP bin/Release/net7.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/
 cd bin/Release/net7.0/publish/wwwroot
 tar cf - ./ | xz -z -9 - > $BASE_DIR/MRU-BIN.tar.xz
-rsync -raP $BASE_DIR/MRU-BIN.tar.xz rory.gay:/data/nginx/html_mru/MRU-BIN.tar.xz
+#rsync -raP $BASE_DIR/MRU-BIN.tar.xz rory.gay:/data/nginx/html_mru/MRU-BIN.tar.xz
 rm -rf $BASE_DIR/MRU-BIN.tar.xz
 cd $BASE_DIR
 git clone .git -b `git branch --show-current` src