mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-24 05:18:58 +00:00
(control-service) Basic GUI for deleting bad links from exploration mode
This commit is contained in:
parent
dd380a5fb3
commit
15912f31d0
@ -1,9 +1,10 @@
|
|||||||
package nu.marginalia.model.id;
|
package nu.marginalia.model.id;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public interface EdgeIdCollection<T> {
|
public interface EdgeIdCollection<T> extends Iterable<EdgeId<T>> {
|
||||||
int size();
|
int size();
|
||||||
boolean isEmpty();
|
boolean isEmpty();
|
||||||
int[] values();
|
int[] values();
|
||||||
@ -12,6 +13,9 @@ public interface EdgeIdCollection<T> {
|
|||||||
return Arrays.stream(values());
|
return Arrays.stream(values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default Iterator<EdgeId<T>> iterator() {
|
||||||
|
return Arrays.stream(values()).mapToObj(EdgeId<T>::new).iterator();
|
||||||
|
}
|
||||||
default EdgeIdArray<T> asArray() {
|
default EdgeIdArray<T> asArray() {
|
||||||
return new EdgeIdArray<>(values());
|
return new EdgeIdArray<>(values());
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,9 @@ import gnu.trove.list.array.TIntArrayList;
|
|||||||
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public record EdgeIdList<T> (TIntArrayList list) implements EdgeIdCollection<T>, EdgeIdCollectionMutable<T> {
|
public record EdgeIdList<T> (TIntArrayList list) implements
|
||||||
|
EdgeIdCollection<T>,
|
||||||
|
EdgeIdCollectionMutable<T> {
|
||||||
|
|
||||||
public EdgeIdList(int... values) { this(new TIntArrayList(values)); }
|
public EdgeIdList(int... values) { this(new TIntArrayList(values)); }
|
||||||
public static <T> EdgeIdList<T> gather(IntStream stream) {
|
public static <T> EdgeIdList<T> gather(IntStream stream) {
|
||||||
|
@ -35,7 +35,7 @@ dependencies {
|
|||||||
implementation project(':code:api:search-api')
|
implementation project(':code:api:search-api')
|
||||||
implementation project(':code:api:index-api')
|
implementation project(':code:api:index-api')
|
||||||
implementation project(':code:api:process-mqapi')
|
implementation project(':code:api:process-mqapi')
|
||||||
|
implementation project(':code:features-search:screenshots')
|
||||||
|
|
||||||
implementation libs.lombok
|
implementation libs.lombok
|
||||||
annotationProcessor libs.lombok
|
annotationProcessor libs.lombok
|
||||||
|
@ -2,6 +2,7 @@ package nu.marginalia.control;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import gnu.trove.list.array.TIntArrayList;
|
||||||
import nu.marginalia.client.ServiceMonitors;
|
import nu.marginalia.client.ServiceMonitors;
|
||||||
import nu.marginalia.control.actor.Actor;
|
import nu.marginalia.control.actor.Actor;
|
||||||
import nu.marginalia.control.model.*;
|
import nu.marginalia.control.model.*;
|
||||||
@ -10,7 +11,9 @@ import nu.marginalia.db.storage.model.FileStorageId;
|
|||||||
import nu.marginalia.db.storage.model.FileStorageType;
|
import nu.marginalia.db.storage.model.FileStorageType;
|
||||||
import nu.marginalia.model.EdgeDomain;
|
import nu.marginalia.model.EdgeDomain;
|
||||||
import nu.marginalia.model.gson.GsonFactory;
|
import nu.marginalia.model.gson.GsonFactory;
|
||||||
|
import nu.marginalia.model.id.EdgeIdList;
|
||||||
import nu.marginalia.renderer.RendererFactory;
|
import nu.marginalia.renderer.RendererFactory;
|
||||||
|
import nu.marginalia.screenshot.ScreenshotService;
|
||||||
import nu.marginalia.service.server.*;
|
import nu.marginalia.service.server.*;
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -35,6 +38,7 @@ public class ControlService extends Service {
|
|||||||
private final ApiKeyService apiKeyService;
|
private final ApiKeyService apiKeyService;
|
||||||
private final DomainComplaintService domainComplaintService;
|
private final DomainComplaintService domainComplaintService;
|
||||||
private final ControlBlacklistService blacklistService;
|
private final ControlBlacklistService blacklistService;
|
||||||
|
private final RandomExplorationService randomExplorationService;
|
||||||
private final ControlActorService controlActorService;
|
private final ControlActorService controlActorService;
|
||||||
private final StaticResources staticResources;
|
private final StaticResources staticResources;
|
||||||
private final MessageQueueService messageQueueService;
|
private final MessageQueueService messageQueueService;
|
||||||
@ -54,7 +58,9 @@ public class ControlService extends Service {
|
|||||||
ApiKeyService apiKeyService,
|
ApiKeyService apiKeyService,
|
||||||
DomainComplaintService domainComplaintService,
|
DomainComplaintService domainComplaintService,
|
||||||
ControlBlacklistService blacklistService,
|
ControlBlacklistService blacklistService,
|
||||||
ControlActionsService controlActionsService
|
ControlActionsService controlActionsService,
|
||||||
|
ScreenshotService screenshotService,
|
||||||
|
RandomExplorationService randomExplorationService
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
|
||||||
super(params);
|
super(params);
|
||||||
@ -64,6 +70,7 @@ public class ControlService extends Service {
|
|||||||
this.apiKeyService = apiKeyService;
|
this.apiKeyService = apiKeyService;
|
||||||
this.domainComplaintService = domainComplaintService;
|
this.domainComplaintService = domainComplaintService;
|
||||||
this.blacklistService = blacklistService;
|
this.blacklistService = blacklistService;
|
||||||
|
this.randomExplorationService = randomExplorationService;
|
||||||
|
|
||||||
var indexRenderer = rendererFactory.renderer("control/index");
|
var indexRenderer = rendererFactory.renderer("control/index");
|
||||||
var eventsRenderer = rendererFactory.renderer("control/events");
|
var eventsRenderer = rendererFactory.renderer("control/events");
|
||||||
@ -75,6 +82,7 @@ public class ControlService extends Service {
|
|||||||
var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs");
|
var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs");
|
||||||
var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls");
|
var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls");
|
||||||
var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed");
|
var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed");
|
||||||
|
var reviewRandomDomainsRenderer = rendererFactory.renderer("control/review-random-domains");
|
||||||
|
|
||||||
var apiKeysRenderer = rendererFactory.renderer("control/api-keys");
|
var apiKeysRenderer = rendererFactory.renderer("control/api-keys");
|
||||||
var domainComplaintsRenderer = rendererFactory.renderer("control/domain-complaints");
|
var domainComplaintsRenderer = rendererFactory.renderer("control/domain-complaints");
|
||||||
@ -117,6 +125,9 @@ public class ControlService extends Service {
|
|||||||
final HtmlRedirect redirectToComplaints = new HtmlRedirect("/complaints");
|
final HtmlRedirect redirectToComplaints = new HtmlRedirect("/complaints");
|
||||||
final HtmlRedirect redirectToMessageQueue = new HtmlRedirect("/message-queue");
|
final HtmlRedirect redirectToMessageQueue = new HtmlRedirect("/message-queue");
|
||||||
|
|
||||||
|
// Needed to be able to show website screenshots
|
||||||
|
Spark.get("/public/screenshot/:id", screenshotService::serveScreenshotRequest);
|
||||||
|
|
||||||
// FSMs
|
// FSMs
|
||||||
|
|
||||||
Spark.post("/public/fsms/:fsm/start", controlActorService::startFsm, redirectToActors);
|
Spark.post("/public/fsms/:fsm/start", controlActorService::startFsm, redirectToActors);
|
||||||
@ -178,11 +189,51 @@ public class ControlService extends Service {
|
|||||||
Spark.post("/public/actions/flush-api-caches", controlActionsService::flushApiCaches, redirectToActors);
|
Spark.post("/public/actions/flush-api-caches", controlActionsService::flushApiCaches, redirectToActors);
|
||||||
Spark.post("/public/actions/truncate-links-database", controlActionsService::truncateLinkDatabase, redirectToActors);
|
Spark.post("/public/actions/truncate-links-database", controlActionsService::truncateLinkDatabase, redirectToActors);
|
||||||
|
|
||||||
|
// Review Random Domains
|
||||||
|
Spark.get("/public/review-random-domains", this::reviewRandomDomainsModel, reviewRandomDomainsRenderer::render);
|
||||||
|
|
||||||
|
Spark.post("/public/review-random-domains", this::reviewRandomDomainsAction);
|
||||||
|
|
||||||
|
|
||||||
Spark.get("/public/:resource", this::serveStatic);
|
Spark.get("/public/:resource", this::serveStatic);
|
||||||
|
|
||||||
monitors.subscribe(this::logMonitorStateChange);
|
monitors.subscribe(this::logMonitorStateChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Object reviewRandomDomainsModel(Request request, Response response) throws SQLException {
|
||||||
|
String afterVal = Objects.requireNonNullElse(request.queryParams("after"), "0");
|
||||||
|
int after = Integer.parseInt(afterVal);
|
||||||
|
var domains = randomExplorationService.getDomains(after, 25);
|
||||||
|
int nextAfter = domains.stream().mapToInt(RandomExplorationService.RandomDomainResult::id).max().orElse(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
return Map.of("domains", domains,
|
||||||
|
"after", nextAfter);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object reviewRandomDomainsAction(Request request, Response response) throws SQLException {
|
||||||
|
TIntArrayList idList = new TIntArrayList();
|
||||||
|
|
||||||
|
request.queryParams().forEach(key -> {
|
||||||
|
if (key.startsWith("domain-")) {
|
||||||
|
String value = request.queryParams(key);
|
||||||
|
if ("on".equalsIgnoreCase(value)) {
|
||||||
|
int id = Integer.parseInt(key.substring(7));
|
||||||
|
idList.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
randomExplorationService.removeRandomDomains(new EdgeIdList<>(idList.toArray()));
|
||||||
|
|
||||||
|
String after = request.queryParams("after");
|
||||||
|
|
||||||
|
return """
|
||||||
|
<?doctype html>
|
||||||
|
<html><head><meta http-equiv="refresh" content="0;URL='/review-random-domains?after=%s'" /></head></html>
|
||||||
|
""".formatted(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private Object blacklistModel(Request request, Response response) {
|
private Object blacklistModel(Request request, Response response) {
|
||||||
return Map.of("blacklist", blacklistService.lastNAdditions(100));
|
return Map.of("blacklist", blacklistService.lastNAdditions(100));
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
package nu.marginalia.control.svc;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import nu.marginalia.model.EdgeDomain;
|
||||||
|
import nu.marginalia.model.id.EdgeIdList;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RandomExplorationService {
|
||||||
|
|
||||||
|
private final HikariDataSource dataSource;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public RandomExplorationService(HikariDataSource dataSource) {
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeRandomDomains(EdgeIdList<EdgeDomain> ids) throws SQLException {
|
||||||
|
try (var conn = dataSource.getConnection();
|
||||||
|
var stmt = conn.prepareStatement("""
|
||||||
|
DELETE FROM EC_RANDOM_DOMAINS
|
||||||
|
WHERE DOMAIN_ID = ?
|
||||||
|
AND DOMAIN_SET = 0
|
||||||
|
"""))
|
||||||
|
{
|
||||||
|
for (var id : ids) {
|
||||||
|
stmt.setInt(1, id.id());
|
||||||
|
stmt.addBatch();
|
||||||
|
}
|
||||||
|
stmt.executeBatch();
|
||||||
|
if (!conn.getAutoCommit()) {
|
||||||
|
conn.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RandomDomainResult> getDomains(int afterId, int numResults) throws SQLException {
|
||||||
|
try (var conn = dataSource.getConnection();
|
||||||
|
var stmt = conn.prepareStatement("""
|
||||||
|
SELECT DOMAIN_ID, DOMAIN_NAME FROM EC_RANDOM_DOMAINS
|
||||||
|
INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_ID
|
||||||
|
WHERE DOMAIN_ID >= ?
|
||||||
|
LIMIT ?
|
||||||
|
"""))
|
||||||
|
{
|
||||||
|
List<RandomDomainResult> ret = new ArrayList<>(numResults);
|
||||||
|
stmt.setInt(1, afterId);
|
||||||
|
stmt.setInt(2, numResults);
|
||||||
|
var rs = stmt.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
ret.add(new RandomDomainResult(rs.getInt(1), rs.getString(2)));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public record RandomDomainResult(int id, String domainName) {}
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
<li><a href="/api-keys" title="Create or remove API keys">API Keys</a></li>
|
<li><a href="/api-keys" title="Create or remove API keys">API Keys</a></li>
|
||||||
<li><a href="/blacklist" title="Add or remove website sanctions">Blacklist</a></li>
|
<li><a href="/blacklist" title="Add or remove website sanctions">Blacklist</a></li>
|
||||||
<li><a href="/complaints" title="View and act on user complaints">Complaints</a></li>
|
<li><a href="/complaints" title="View and act on user complaints">Complaints</a></li>
|
||||||
|
<li><a href="/review-random-domains" title="Review random domains list">Random Exploration</a></li>
|
||||||
<li>System</li>
|
<li>System</li>
|
||||||
<li><a href="/actions" title="Trigger system actions">Actions</a></li>
|
<li><a href="/actions" title="Trigger system actions">Actions</a></li>
|
||||||
<li><a href="/storage" title="View file storage, trigger crawling and processing">Storage</a></li>
|
<li><a href="/storage" title="View file storage, trigger crawling and processing">Storage</a></li>
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Control Service</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{> control/partials/nav}}
|
||||||
|
<section>
|
||||||
|
<h1>Domain Review</h1>
|
||||||
|
<form method="POST" action="/review-random-domains">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Action</th><th>Domain Name</th><th>Screenshot</th>
|
||||||
|
</tr>
|
||||||
|
{{#each domains}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="domain-{{id}}" id="domain-{{id}}"/> <label for="domain-{{id}}">Remove</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://{{domainName}}">{{domainName}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<img src="/screenshot/{{id}}" style="max-width: 100%; max-height: 100px"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3"><input type="Submit" value="Process"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<input type="hidden" name="after" value="{{after}}" />
|
||||||
|
</form>
|
||||||
|
</section>
|
Loading…
Reference in New Issue
Block a user