R1 cleanup: collAnon and dead code gone, no IndexedDB, honest PWA files
Build / Build (push) Successful in 51s

Starts the completion program (SocialPub docs/ROADMAP.md, "Owner decisions on finishing everything", 2026-10-17).

- The localizer's namespace is decePubClient.Services; collAnon is gone from the repo.
- Dead code: PagesBase, VConstants, SettingsOptions, ViewLanguage, unused APIs routes, InstanceService.Self,
  RequestStats.LastOf/AverageMs, PostAnon/DeleteTotallyAnon, unused AppStatusService methods and their main.js helpers
  (the app badge stays for Web Push), ExtensionMethods/SUtility leftovers (Markdig and BlazorZXingJs packages go),
  unused PublicCacheData/PageSettings/AppConfiguration fields, commented-out code.
- The client log in IndexedDB, which nothing ever read, goes with the whole IndexedDB layer (DnetIndexedDb, IStorage):
  logs go to the browser console; main.js deletes the old "data" database once.
- The solution names the real ClientModels project. Unused vendor assets (fontawesome, solid, open-iconic, rxjs) go.
- robots.txt disallows everything; manifest.json and index.html say decePub; the service worker's cache name follows
  each publish's assets version, so old caches are purged.
- MessageType is Visibility (member names kept: they are resx keys); ExpandMessage.razor is Thread.razor (same route).
- i18n: dead keys removed, Italian "camera" is "punto di vista", DInputBase/DCalendar validation messages come from
  ErrorMessages, DataTransfer's spinner has its label.
- CLAUDE.md follows: client storage, JS interop, PWA, the D fields' messages.

Verified: dotnet build decePubClient.csproj and decePubClient.sln -c Release (0 errors, no new warnings); a Pasture
publish served by the pasture's decepub peer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 02:56:57 +02:00
1 parent e4414f1ac9
commit fd2f2e7c65
79 files changed
+155 -6673

No files matched your search

-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
+1 -26
View File
@@ -1,5 +1,5 @@
using Microsoft.AspNetCore.Components;
using decePubClient.Models;
using decePubClient.Models;
using Microsoft.JSInterop;
@@ -12,29 +12,12 @@ public class AppStatusService(IJSRuntime jsRuntime)
public async ValueTask<bool> IsOnline(CancellationToken cancellationToken = default) =>
await jsRuntime.InvokeAsync<bool>("isOnline", cancellationToken);
public bool IsServiceWorkerAvailable() => JsSyncRuntime.Invoke<bool>("isServiceWorkerAvailable");
public string Language() => JsSyncRuntime.Invoke<string>("getLanguage");
public void Language(string language) => JsSyncRuntime.InvokeVoid("setLanguage", language);
public long GetHeight(string classId) => JsSyncRuntime.Invoke<long>("getHeight", classId);
public void SetAppBadge(int counter) => JsSyncRuntime.InvokeVoid("setAppBadge", counter);
public void ClearAppBadge() => JsSyncRuntime.InvokeVoid("clearAppBadge");
public async ValueTask ClearCache(CancellationToken cancellationToken = default) =>
await jsRuntime.InvokeVoidAsync("clearCache", cancellationToken);
public async ValueTask ClearLocalStorage(CancellationToken cancellationToken = default) =>
await jsRuntime.InvokeVoidAsync("clearLocalStorage", cancellationToken);
public void ReloadPage()
{
JsSyncRuntime.InvokeVoid("reloadPage");
}
// wwwroot/js/theme.js
public void ApplyTheme(PageSettings pageSettings) => JsSyncRuntime.InvokeVoid("neoTheme.apply", pageSettings);
@@ -55,12 +38,4 @@ public class AppStatusService(IJSRuntime jsRuntime)
// a sideways-scrolling strip of tabs shows its selected tab
public void RevealActive(ElementReference strip) => JsSyncRuntime.InvokeVoid("revealActive", strip);
public bool CanShare() => JsSyncRuntime.Invoke<bool>("canShareStuff");
public async ValueTask ShareTextUrl(string title, string text, Uri uri, CancellationToken cancellationToken = default) =>
await jsRuntime.InvokeVoidAsync("shareTextUrl", cancellationToken, title, text, uri.ToString());
public async ValueTask ShareText(string title, string text, CancellationToken cancellationToken = default) =>
await jsRuntime.InvokeVoidAsync("shareText", cancellationToken, title, text);
}
-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
+1 -3
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Helpers;
using decePubClient.Models;
@@ -190,7 +188,7 @@ namespace decePubClient.Services
await LogFailure(response, nameof(Logout), cancellationToken);
}
await authStateProvider.LogoutAsync(deleteDb: true, cancellationToken);
await authStateProvider.LogoutAsync(cancellationToken);
}
async Task LogFailure(HttpResponseMessage response, string caller, CancellationToken cancellationToken)
+1 -1
View File
@@ -4,7 +4,7 @@ using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels.Resources;
namespace collAnon.Client.Services
namespace decePubClient.Services
{
/// <summary>
/// The app's localizer: looks a key up in Resources/AllStrings, then in PrivaPub.ClientModels' FieldsNameResource and
-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
-16
View File
@@ -3,8 +3,6 @@ using System.Globalization;
using System.Net;
using System.Net.Http.Json;
using collAnon.Client.Services;
using decePubClient.Helpers;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
@@ -35,8 +33,6 @@ namespace decePubClient.Services
Task<HttpResponseMessage> Post(string uri, object payload = default, bool retryOnError = false, CancellationToken cancellationToken = default);
Task<HttpResponseMessage> PostAnon(string uri, object payload = default, bool retryOnError = false, CancellationToken cancellationToken = default);
Task<HttpResponseMessage> Put(string uri, object payload = default, bool retryOnError = false, CancellationToken cancellationToken = default);
Task<HttpResponseMessage> Patch(string uri, object payload = default, bool retryOnError = false, CancellationToken cancellationToken = default);
@@ -47,9 +43,6 @@ namespace decePubClient.Services
Task<HttpResponseMessage> Delete(string uri, string[] queryParams = default, object payload = default, bool retryOnError = false,
CancellationToken cancellationToken = default);
Task<HttpResponseMessage> DeleteTotallyAnon(string uri, string[] queryParams = default, object payload = default, bool retryOnError = false,
CancellationToken cancellationToken = default);
/// <summary>
/// Exchanges the root's JWT for the persona's Mastodon token (PrivaPub's PersonaExchange) and makes the persona
/// current. A failure comes back like any other.
@@ -88,10 +81,6 @@ namespace decePubClient.Services
public Task<HttpResponseMessage> Patch(string uri, object payload = default, bool retryOnError = false, CancellationToken cancellationToken = default) =>
Send(nameof(Patch), () => BuildRequest(HttpMethod.Patch, uri, payload, forceContent: true), SendAuthRequest, retryOnError, cancellationToken);
public Task<HttpResponseMessage> PostAnon(string uri, object payload = default, bool retryOnError = false, CancellationToken cancellationToken = default) =>
Send(nameof(PostAnon), () => BuildRequest(HttpMethod.Post, uri, payload, forceContent: true), SendAuthIfAvailableRequest, retryOnError,
cancellationToken);
public Task<HttpResponseMessage> PostTotallyAnon(string uri, object payload = default, bool retryOnError = false,
CancellationToken cancellationToken = default) =>
Send(nameof(PostTotallyAnon), () => BuildRequest(HttpMethod.Post, uri, payload, forceContent: true), SendAnonymousRequest, retryOnError,
@@ -101,11 +90,6 @@ namespace decePubClient.Services
CancellationToken cancellationToken = default) =>
Send(nameof(Delete), () => BuildRequest(HttpMethod.Delete, uri, payload, queryParams), SendAuthRequest, retryOnError, cancellationToken);
public Task<HttpResponseMessage> DeleteTotallyAnon(string uri, string[] queryParams = default, object payload = default, bool retryOnError = false,
CancellationToken cancellationToken = default) =>
Send(nameof(DeleteTotallyAnon), () => BuildRequest(HttpMethod.Delete, uri, payload, queryParams), SendAnonymousRequest, retryOnError,
cancellationToken);
public async Task<HttpResponseMessage> ExchangePersonaToken(string personaId, CancellationToken cancellationToken = default)
{
var jwt = await authStateProvider.GetToken(cancellationToken);
+11 -35
View File
@@ -11,51 +11,27 @@ public interface ILoggingService
void Process(string message, string where = default, LogLevel level = LogLevel.Information);
}
/// <summary>
/// Writes to the browser console through ILogger and keeps errors and warnings in the IndexedDB client log, so they
/// survive a reload. Never throws: logging must not break the code that is reporting a failure.
/// </summary>
public class LoggingService(IStorage storage, ILogger<LoggingService> logger) : ILoggingService
/// <summary>Writes to the browser console through ILogger.</summary>
public class LoggingService(ILogger<LoggingService> logger) : ILoggingService
{
public void Process(string message, string where = default, LogLevel level = LogLevel.Information) =>
logger.Log(level, "[{Where}] {Message}", where ?? "N/A", message);
public async ValueTask ProcessError(Exception exception, string where, CancellationToken cancellationToken = default)
public ValueTask ProcessError(Exception exception, string where, CancellationToken cancellationToken = default)
{
try
{
logger.LogError(exception, "[{Where}]", where);
await storage.AddLog(exception, where, cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, $"{nameof(LoggingService)}.{nameof(ProcessError)}");
}
logger.LogError(exception, "[{Where}]", where);
return ValueTask.CompletedTask;
}
public async ValueTask ProcessError(string message, string where, CancellationToken cancellationToken = default)
public ValueTask ProcessError(string message, string where, CancellationToken cancellationToken = default)
{
try
{
logger.LogError("[{Where}] {Message}", where, message);
await storage.AddLog(message, where, cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, $"{nameof(LoggingService)}.{nameof(ProcessError)}");
}
logger.LogError("[{Where}] {Message}", where, message);
return ValueTask.CompletedTask;
}
public async ValueTask ProcessWarning(string message, string where, CancellationToken cancellationToken = default)
public ValueTask ProcessWarning(string message, string where, CancellationToken cancellationToken = default)
{
try
{
logger.LogWarning("[{Where}] {Message}", where, message);
await storage.AddLog(message, where, cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, $"{nameof(LoggingService)}.{nameof(ProcessWarning)}");
}
logger.LogWarning("[{Where}] {Message}", where, message);
return ValueTask.CompletedTask;
}
}
-123
View File
@@ -1,123 +0,0 @@
using decePubClient.Helpers;
using decePubClient.Models;
using PrivaPub.ClientModels;
namespace decePubClient.Services
{
/// <summary>
/// The IndexedDB database "data" (schema in GenericExtensions.AddIndexedDb): the client log. Lists come back empty on
/// failure, mutations as an invalid WebResult; nothing throws. Failures are logged with ILogger only, because
/// ILoggingService writes its log through this class.
/// </summary>
public interface IStorage
{
Task<List<ClientLogs>> GetClientLogs(CancellationToken cancellationToken = default);
Task AddLog(Exception exception, string where, CancellationToken cancellationToken = default);
Task AddLog(string message, string where, CancellationToken cancellationToken = default);
Task<WebResult> RemoveAll(bool includeClientLogs = false, CancellationToken cancellationToken = default);
}
public class Storage(IndexedDb db, ILogger<Storage> logger) : IStorage
{
/// <summary>The client log keeps at most this many entries; past it the oldest half is dropped.</summary>
const int MaxLogs = 100;
#region Logs
public async Task<List<ClientLogs>> GetClientLogs(CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
await db.OpenIndexedDb();
var logs = await db.GetAll<ClientLogs>(nameof(ClientLogs)) ?? [];
return [.. logs.OrderByDescending(log => log.TimeStamp)];
}
catch (Exception ex)
{
logger.LogError(ex, $"{nameof(Storage)}.{nameof(GetClientLogs)}");
return [];
}
}
public Task AddLog(Exception exception, string where, CancellationToken cancellationToken = default) =>
AddLog(new ClientLogs
{
Where = where,
Exception = new()
{
Type = exception.GetType().ToString(),
HelpLink = exception.HelpLink,
HResult = exception.HResult.ToString(),
InnerExceptionMessage = exception.InnerException?.Message,
Message = exception.Message,
Source = exception.Source,
StackTrace = exception.StackTrace,
TargetSiteName = exception.TargetSite?.Name
}
}, cancellationToken);
public Task AddLog(string message, string where, CancellationToken cancellationToken = default) =>
AddLog(new ClientLogs { Where = where, WarningMessage = message }, cancellationToken);
/// <summary>Numbers the entry after the newest one and trims the log; never throws.</summary>
async Task AddLog(ClientLogs log, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
await db.OpenIndexedDb();
var maxKey = await db.GetMaxKey<long>(nameof(ClientLogs));
var minKey = await db.GetMinKey<long>(nameof(ClientLogs));
if (maxKey - minKey >= MaxLogs)
for (var key = minKey; key < minKey + MaxLogs / 2; key++)
_ = await db.DeleteByKey(nameof(ClientLogs), key);
log.Id = maxKey + 1;
var result = await db.AddItems<ClientLogs>(nameof(ClientLogs), [log]);
logger.LogDebug($"{nameof(AddLog)}() add logs result = {result}");
}
catch (Exception ex)
{
logger.LogError(ex, $"{nameof(Storage)}.{nameof(AddLog)}");
}
}
#endregion
public Task<WebResult> RemoveAll(bool includeClientLogs = false, CancellationToken cancellationToken = default) =>
Run(nameof(RemoveAll), async () =>
{
if (!includeClientLogs)
return "nothing to remove";
return await db.DeleteAll(nameof(ClientLogs));
}, cancellationToken);
/// <summary>Runs a write and turns its outcome into a WebResult.</summary>
async Task<WebResult> Run(string caller, Func<Task<string>> write, CancellationToken cancellationToken)
{
var result = new WebResult();
try
{
cancellationToken.ThrowIfCancellationRequested();
await db.OpenIndexedDb();
logger.LogInformation("{Caller}: {Result}", caller, await write());
return result;
}
catch (OperationCanceledException)
{
return SUtility.DefaultTaskCancelledResult;
}
catch (Exception ex)
{
logger.LogError(ex, $"{nameof(Storage)}.{caller}");
return result.Invalidate(ex.Message, exception: ex);
}
}
}
}
-10
View File
@@ -1,10 +0,0 @@
using DnetIndexedDb;
using Microsoft.JSInterop;
namespace decePubClient.Services
{
public class IndexedDb(IJSRuntime jsRuntime, IndexedDbOptions<IndexedDb> options) : IndexedDbInterop(jsRuntime, options)
{
}
}
-6
View File
@@ -41,12 +41,6 @@ namespace decePubClient.Services
public ServerInfo Known(string host) => host is not null && servers.TryGetValue(host, out var server) && !server.Unknown ? server : null;
public async Task<ServerInfo> Self(CancellationToken cancellationToken = default)
{
await Resolve([SelfHost], cancellationToken);
return Known(SelfHost);
}
/// <summary>Reads the servers not known yet (or known for too long) from PrivaPub.</summary>
public async Task Resolve(IEnumerable<string> hosts, CancellationToken cancellationToken = default)
{
-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
-2
View File
@@ -1,8 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
-1
View File
@@ -1,4 +1,3 @@
using collAnon.Client.Services;
using System.Text;
using decePubClient.Extensions;
-10
View File
@@ -45,16 +45,6 @@ namespace decePubClient.Services
}
}
/// <summary>The newest request to a route template, or null.</summary>
public RequestStat LastOf(string route) => Latest().FirstOrDefault(stat => stat.Route == route);
/// <summary>The mean duration of the successful requests recorded, in milliseconds.</summary>
public double? AverageMs()
{
var answered = Latest().Where(stat => stat.Status is >= 200 and < 400).ToList();
return answered.Count == 0 ? null : answered.Average(stat => stat.DurationMs);
}
/// <summary>The route without its query, with ids replaced by :id, so requests to the same endpoint group.</summary>
public static string Template(string uri)
{
+4 -6
View File
@@ -1,8 +1,6 @@
using System.Globalization;
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
@@ -53,7 +51,7 @@ namespace decePubClient.Services
InReplyToId = form.InReplyToId,
Sensitive = form.HasContentWarning || form.Sensitive && form.Media.Count > 0,
SpoilerText = form.HasContentWarning ? form.SpoilerText : null,
Visibility = form.Visibility,
Visibility = form.MastodonVisibility,
Language = form.Language ?? CultureInfo.CurrentUICulture.TwoLetterISOLanguageName,
ScheduledAt = form.ScheduledAtUtc?.ToString("o", CultureInfo.InvariantCulture),
Poll = form.HasPoll ? new PollForm { Options = [.. form.PollOptions.Select(o => o.Trim()).Where(o => o.Length > 0)], ExpiresIn = form.PollExpiresIn, Multiple = form.PollMultiple } : null,
@@ -188,10 +186,10 @@ namespace decePubClient.Services
AnsweringToPostId = form.InReplyToId,
HasContentWarning = form.HasContentWarning,
SpoilerText = form.HasContentWarning ? form.SpoilerText : null,
Visibility = form.MessageType switch
Visibility = form.Visibility switch
{
MessageType.FollowersOnly => "followersonly",
MessageType.Unlisted => "unlisted",
Visibility.FollowersOnly => "followersonly",
Visibility.Unlisted => "unlisted",
_ => "public"
},
Latitude = form.IsLocated ? form.Latitude : null,
-2
View File
@@ -1,7 +1,5 @@
using System.Net;
using collAnon.Client.Services;
using decePubClient.Extensions;
using decePubClient.Models;
using decePubClient.Models.Mastodon;
+2 -4
View File
@@ -15,7 +15,7 @@ namespace decePubClient.Services
/// The session (AuthData in localStorage, cached in memory) and the authentication state built from it. It only
/// stores and hands out tokens; signing in, exchanging and revoking them is AuthService's job.
/// </summary>
public class TokenAuthStateProvider(ILocalStorageService storage, IStorage dbStorage, ILogger<TokenAuthStateProvider> logger)
public class TokenAuthStateProvider(ILocalStorageService storage, ILogger<TokenAuthStateProvider> logger)
: AuthenticationStateProvider
{
/// <summary>The claim naming the persona the client acts as (an avatar id).</summary>
@@ -89,14 +89,12 @@ namespace decePubClient.Services
await Save(session, cancellationToken);
}
public async Task LogoutAsync(bool deleteDb = false, CancellationToken cancellationToken = default)
public async Task LogoutAsync(CancellationToken cancellationToken = default)
{
logger.LogInformation($"set null({nameof(LogoutAsync)})");
authData = null;
loaded = true;
await storage.RemoveItemAsync(nameof(AuthData), cancellationToken);
if (deleteDb)
await dbStorage.RemoveAll(includeClientLogs: true, cancellationToken: cancellationToken);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}