✨ Add debug page for element loader
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 3 Oct 2026 20:04:28 +0000 (22:04 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 4 Oct 2026 18:54:52 +0000 (20:54 +0200)
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/kotlin/net/pterodactylus/sone/web/pages/ElementsPage.kt [new file with mode: 0644]
src/main/resources/i18n/sone.de.properties
src/main/resources/i18n/sone.en.properties
src/main/resources/templates/elements.html [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/ElementsPageTest.kt [new file with mode: 0644]

index 27ef8d5..9fe4c87 100644 (file)
@@ -23,6 +23,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.core.DefaultElementLoader;
 import net.pterodactylus.sone.core.ElementLoader;
 import net.pterodactylus.sone.core.event.*;
 import net.pterodactylus.sone.data.Sone;
@@ -292,6 +293,7 @@ public class WebInterface implements SessionProvider {
                pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
                pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
+               pageToadletRegistry.addDebugPage(new ElementsPage(this, loaders, templateRenderer, (DefaultElementLoader) elementLoader));
                pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
                pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
                pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/ElementsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/ElementsPage.kt
new file mode 100644 (file)
index 0000000..4250704
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.web.pages
+
+import jakarta.inject.Inject
+import net.pterodactylus.sone.core.DefaultElementLoader
+import net.pterodactylus.sone.main.Loaders
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.MenuName
+import net.pterodactylus.sone.web.page.SoneRequest
+import net.pterodactylus.sone.web.page.TemplatePath
+import net.pterodactylus.sone.web.page.TemplateRenderer
+import net.pterodactylus.sone.web.page.ToadletPath
+import net.pterodactylus.util.template.TemplateContext
+
+@MenuName("Elements")
+@TemplatePath("/templates/elements.html")
+@ToadletPath("elements.html")
+class ElementsPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer, private val elementsLoader: DefaultElementLoader) :
+       SoneTemplatePage(webInterface, loaders, templateRenderer, pageTitleKey = "Page.ElementLoader.Title") {
+
+       override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) {
+               templateContext.set("loadingElements", elementsLoader.loadingElements)
+               templateContext.set("loadedElements", elementsLoader.loadedElements)
+               templateContext.set("rejectedElements", elementsLoader.rejectedElements)
+               templateContext.set("failedElements", elementsLoader.failedElements)
+       }
+
+}
index 6b212f4..7bf5e5a 100644 (file)
@@ -28,6 +28,8 @@ Navigation.Menu.Sone.Item.About.Name=Über Sone
 Navigation.Menu.Sone.Item.About.Tooltip=Informationen über Sone
 Navigation.Menu.Sone.Item.Metrics.Name=Metriken
 Navigation.Menu.Sone.Item.Metrics.Tooltip=Von Sone gesammelte Metriken
+Navigation.Menu.Sone.Item.Elements.Name=Elemente
+Navigation.Menu.Sone.Item.Elements.Tooltip=Informationen über geladene Elemente
 
 Page.About.Title=Über  - Sone
 Page.About.Page.Title=Über
@@ -331,6 +333,17 @@ Page.Metrics.SoneInsertDuration.Title=Hochladedauer einer Sone
 Page.Metrics.SoneParseDuration.Title=Parsdauer einer Sone
 Page.Metrics.ConfigurationSaveDuration.Title=Konfigurationsspeicherdauer
 
+Page.ElementLoader.Title=Elementlader
+Page.ElementLoader.Page.Title=Elementlader
+Page.ElementLoader.Section.LoadingElements.Title=Ladende Elemente
+Page.ElementLoader.Section.LoadingElements.Description=Dieser Abschnitt zeigt Elemente, die gerade geladen werden.
+Page.ElementLoader.Section.LoadedElements.Title=Geladene Elemente
+Page.ElementLoader.Section.LoadedElements.Description=Dieser Abschnitt zeigt Elemente, die bereits erfolgreich geladen wurden, und die in Sone angezeigt werden können.
+Page.ElementLoader.Section.RejectedElements.Title=Verweigerte Elemente
+Page.ElementLoader.Section.RejectedElements.Description=Dieser Abschnitt zeigt Element, die nicht geladen werden, weil sie die Anforderungen nicht erfüllen.
+Page.ElementLoader.Section.FailedElements.Title=Fehlgeschlagene Elemente
+Page.ElementLoader.Section.FailedElements.Description=Dieser Abschnitt zeigt Elemente, die aufgrund eines Fehlers nicht geladen werden konnten. Elemente werden nach 30 Minuten aus dieser Liste entfernt.
+
 View.Search.Button.Search=Suchen
 
 View.CreateSone.Text.WotIdentityRequired=Um eine Sone anzulegen, brauchen Sie eine Identität aus dem {link}„Web of Trust“ Plugin{/link}.
index f06a204..86fdf39 100644 (file)
@@ -28,6 +28,8 @@ Navigation.Menu.Sone.Item.About.Name=About
 Navigation.Menu.Sone.Item.About.Tooltip=Information about Sone
 Navigation.Menu.Sone.Item.Metrics.Name=Metrics
 Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
+Navigation.Menu.Sone.Item.Elements.Name=Elements
+Navigation.Menu.Sone.Item.Elements.Tooltip=Information about Loaded Elements
 
 Page.About.Title=About - Sone
 Page.About.Page.Title=About
@@ -331,6 +333,17 @@ Page.Metrics.SoneInsertDuration.Title=Sone Insert Duration
 Page.Metrics.SoneParseDuration.Title=Sone Parse Duration
 Page.Metrics.ConfigurationSaveDuration.Title=Configuration Save Duration
 
+Page.ElementLoader.Title=Element Loader
+Page.ElementLoader.Page.Title=Element Loader
+Page.ElementLoader.Section.LoadingElements.Title=Loading Elements
+Page.ElementLoader.Section.LoadingElements.Description=This section lists elements that are currently being loaded.
+Page.ElementLoader.Section.LoadedElements.Title=Loaded Elements
+Page.ElementLoader.Section.LoadedElements.Description=This section lists elements that have successfully been loaded and can be displayed in Sone.
+Page.ElementLoader.Section.RejectedElements.Title=Rejected Elements
+Page.ElementLoader.Section.RejectedElements.Description=This section lists elements that were loaded, but rejected.
+Page.ElementLoader.Section.FailedElements.Title=Failed Elements
+Page.ElementLoader.Section.FailedElements.Description=This section lists elements that could not be loaded. Elements in this list are removed after 30 minutes.
+
 View.Search.Button.Search=Search
 
 View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
diff --git a/src/main/resources/templates/elements.html b/src/main/resources/templates/elements.html
new file mode 100644 (file)
index 0000000..42199fd
--- /dev/null
@@ -0,0 +1,75 @@
+<%include include/head.html>
+
+<h1><%= Page.ElementLoader.Page.Title|l10n|html></h1>
+
+<h2><%= Page.ElementLoader.Section.LoadingElements.Title|l10n|html></h2>
+<p><%= Page.ElementLoader.Section.LoadingElements.Description|l10n|html></p>
+
+<%foreach loadingElements loadingElement>
+    <%first>
+        <ul class="loading-elements">
+    <%/first>
+            <li class="key"><a href="/<% loadingElement>"><% loadingElement></a></li>
+    <%last>
+        </ul>
+    <%/last>
+<%/foreach>
+
+<h2><%= Page.ElementLoader.Section.LoadedElements.Title|l10n|html></h2>
+<p><%= Page.ElementLoader.Section.LoadedElements.Description|l10n|html></p>
+
+<%foreach loadedElements loadedElement>
+    <%first>
+        <ul class="loaded-elements">
+    <%/first>
+            <li class="key">
+                <a href="/<% loadedElement.link>"><% loadedElement.link></a>
+                <%foreach loadedElement.properties.entries property>
+                    <%first>
+                        <ul>
+                    <%/first>
+                            <li class="property-entry">
+                                <span class="key"><% property.key></span>:
+                                <span class="value"><% property.value></span>
+                            </li>
+                    <%last>
+                        </ul>
+                    <%/last>
+                <%/foreach>
+            </li>
+    <%last>
+        </ul>
+    <%/last>
+<%/foreach>
+
+<h2><%= Page.ElementLoader.Section.RejectedElements.Title|l10n|html></h2>
+<p><%= Page.ElementLoader.Section.RejectedElements.Description|l10n|html></p>
+
+<%foreach rejectedElements rejectedElement>
+<%first>
+<ul class="rejected-elements">
+    <%/first>
+    <li class="key">
+        <a href="/<% rejectedElement.key>"><% rejectedElement.key></a>: <% rejectedElement.value.reason> (<% rejectedElement.value.data>)
+    </li>
+    <%last>
+</ul>
+<%/last>
+<%/foreach>
+
+<h2><%= Page.ElementLoader.Section.FailedElements.Title|l10n|html></h2>
+<p><%= Page.ElementLoader.Section.FailedElements.Description|l10n|html></p>
+
+<%foreach failedElements failedElement>
+<%first>
+<ul class="failed-elements">
+    <%/first>
+    <li class="key">
+        <a href="/<% failedElement>"><% failedElement></a>
+    </li>
+    <%last>
+</ul>
+<%/last>
+<%/foreach>
+
+<%include include/tail.html>
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/ElementsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/ElementsPageTest.kt
new file mode 100644 (file)
index 0000000..32cc6ae
--- /dev/null
@@ -0,0 +1,89 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.inject.Guice.createInjector
+import kotlin.test.Test
+import net.pterodactylus.sone.core.DefaultElementLoader
+import net.pterodactylus.sone.core.DefaultElementLoader.Rejected
+import net.pterodactylus.sone.core.DefaultElementLoader.RejectionReason.InvalidMediaType
+import net.pterodactylus.sone.core.DefaultElementLoader.RejectionReason.SizeTooLarge
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.isProvidedByDeepMock
+import net.pterodactylus.sone.test.isProvidedByMock
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.TemplateRenderer
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.hasEntry
+
+class ElementsPageTest : WebPageTest() {
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("elements.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.ElementLoader.Title", "elements page title")
+               assertThat(page.getPageTitle(soneRequest), equalTo("elements page title"))
+       }
+
+       @Test
+       fun `page lists loading elements`() {
+               whenever(elementsLoader.loadingElements).thenReturn(setOf("key 1", "key 2"))
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["loadingElements"] as Collection<*>, containsInAnyOrder("key 1", "key 2"))
+       }
+
+       @Test
+       fun `page lists loaded elements`() {
+               whenever(elementsLoader.loadedElements).thenReturn(setOf(LinkedElement("key 3"), LinkedElement("key 4")))
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["loadedElements"] as Collection<*>, containsInAnyOrder(LinkedElement("key 3"), LinkedElement("key 4")))
+       }
+
+       @Test
+       fun `page lists rejected elements`() {
+               whenever(elementsLoader.rejectedElements).thenReturn(mapOf(
+                       "key 5" to Rejected(InvalidMediaType, "application/test"),
+                       "key 6" to Rejected(SizeTooLarge, 1234567L),
+               ))
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["rejectedElements"] as Map<*, *>, allOf(
+                       hasEntry("key 5", Rejected(InvalidMediaType, "application/test")),
+                       hasEntry("key 6", Rejected(SizeTooLarge, 1234567L)),
+               ))
+       }
+
+       @Test
+       fun `page lists failed elements`() {
+               whenever(elementsLoader.failedElements).thenReturn(setOf("key 7", "key 8"))
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["failedElements"] as Collection<*>, containsInAnyOrder("key 7", "key 8"))
+       }
+
+       @Test
+       fun `page can be created by Guice`() {
+               val injector = createInjector(
+                       WebInterface::class.isProvidedByDeepMock(),
+                       TemplateRenderer::class.isProvidedByMock(),
+                       DefaultElementLoader::class.isProvidedByMock(),
+               )
+               injector.getInstance<ElementsPage>()
+       }
+
+       private val elementsLoader: DefaultElementLoader = mock<DefaultElementLoader>()
+       override val page: SoneTemplatePage
+               get() = ElementsPage(webInterface, loaders, templateRenderer, elementsLoader)
+
+}