(conf) Introduce a new concept of node profiles

Node profiles decide which actors are started, and which views are available in the control GUI.  This helps keep the system organized, and hides real-time clutter from the batch-oriented nodes.
This commit is contained in:
Viktor Lofgren 2024-11-20 18:15:22 +01:00
parent f94911541a
commit 47dfbacb00
16 changed files with 211 additions and 47 deletions

View File

@ -3,6 +3,7 @@ package nu.marginalia.nodecfg;
import com.google.inject.Inject;
import com.zaxxer.hikari.HikariDataSource;
import nu.marginalia.nodecfg.model.NodeConfiguration;
import nu.marginalia.nodecfg.model.NodeProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -20,10 +21,10 @@ public class NodeConfigurationService {
this.dataSource = dataSource;
}
public NodeConfiguration create(int id, String description, boolean acceptQueries, boolean keepWarcs) throws SQLException {
public NodeConfiguration create(int id, String description, boolean acceptQueries, boolean keepWarcs, NodeProfile nodeProfile) throws SQLException {
try (var conn = dataSource.getConnection();
var is = conn.prepareStatement("""
INSERT IGNORE INTO NODE_CONFIGURATION(ID, DESCRIPTION, ACCEPT_QUERIES, KEEP_WARCS) VALUES(?, ?, ?, ?)
INSERT IGNORE INTO NODE_CONFIGURATION(ID, DESCRIPTION, ACCEPT_QUERIES, KEEP_WARCS, NODE_PROFILE) VALUES(?, ?, ?, ?, ?)
""")
)
{
@ -31,6 +32,7 @@ public class NodeConfigurationService {
is.setString(2, description);
is.setBoolean(3, acceptQueries);
is.setBoolean(4, keepWarcs);
is.setString(5, nodeProfile.name());
if (is.executeUpdate() <= 0) {
throw new IllegalStateException("Failed to insert configuration");
@ -43,7 +45,7 @@ public class NodeConfigurationService {
public List<NodeConfiguration> getAll() {
try (var conn = dataSource.getConnection();
var qs = conn.prepareStatement("""
SELECT ID, DESCRIPTION, ACCEPT_QUERIES, AUTO_CLEAN, PRECESSION, KEEP_WARCS, DISABLED
SELECT ID, DESCRIPTION, ACCEPT_QUERIES, AUTO_CLEAN, PRECESSION, KEEP_WARCS, NODE_PROFILE, DISABLED
FROM NODE_CONFIGURATION
""")) {
var rs = qs.executeQuery();
@ -58,6 +60,7 @@ public class NodeConfigurationService {
rs.getBoolean("AUTO_CLEAN"),
rs.getBoolean("PRECESSION"),
rs.getBoolean("KEEP_WARCS"),
NodeProfile.valueOf(rs.getString("NODE_PROFILE")),
rs.getBoolean("DISABLED")
));
}
@ -72,7 +75,7 @@ public class NodeConfigurationService {
public NodeConfiguration get(int nodeId) throws SQLException {
try (var conn = dataSource.getConnection();
var qs = conn.prepareStatement("""
SELECT ID, DESCRIPTION, ACCEPT_QUERIES, AUTO_CLEAN, PRECESSION, KEEP_WARCS, DISABLED
SELECT ID, DESCRIPTION, ACCEPT_QUERIES, AUTO_CLEAN, PRECESSION, KEEP_WARCS, NODE_PROFILE, DISABLED
FROM NODE_CONFIGURATION
WHERE ID=?
""")) {
@ -86,6 +89,7 @@ public class NodeConfigurationService {
rs.getBoolean("AUTO_CLEAN"),
rs.getBoolean("PRECESSION"),
rs.getBoolean("KEEP_WARCS"),
NodeProfile.valueOf(rs.getString("NODE_PROFILE")),
rs.getBoolean("DISABLED")
);
}
@ -98,7 +102,7 @@ public class NodeConfigurationService {
try (var conn = dataSource.getConnection();
var us = conn.prepareStatement("""
UPDATE NODE_CONFIGURATION
SET DESCRIPTION=?, ACCEPT_QUERIES=?, AUTO_CLEAN=?, PRECESSION=?, KEEP_WARCS=?, DISABLED=?
SET DESCRIPTION=?, ACCEPT_QUERIES=?, AUTO_CLEAN=?, PRECESSION=?, KEEP_WARCS=?, DISABLED=?, NODE_PROFILE=?
WHERE ID=?
"""))
{
@ -108,7 +112,8 @@ public class NodeConfigurationService {
us.setBoolean(4, config.includeInPrecession());
us.setBoolean(5, config.keepWarcs());
us.setBoolean(6, config.disabled());
us.setInt(7, config.node());
us.setString(7, config.profile().name());
us.setInt(8, config.node());
if (us.executeUpdate() <= 0)
throw new IllegalStateException("Failed to update configuration");

View File

@ -6,6 +6,7 @@ public record NodeConfiguration(int node,
boolean autoClean,
boolean includeInPrecession,
boolean keepWarcs,
NodeProfile profile,
boolean disabled
)
{

View File

@ -0,0 +1,28 @@
package nu.marginalia.nodecfg.model;
public enum NodeProfile {
BATCH_CRAWL,
REALTIME,
MIXED,
SIDELOAD;
public boolean isBatchCrawl() {
return this == BATCH_CRAWL;
}
public boolean isRealtime() {
return this == REALTIME;
}
public boolean isMixed() {
return this == MIXED;
}
public boolean isSideload() {
return this == SIDELOAD;
}
public boolean permitBatchCrawl() {
return isBatchCrawl() ||isMixed();
}
public boolean permitSideload() {
return isMixed() || isSideload();
}
}

View File

@ -0,0 +1 @@
ALTER TABLE WMSA_prod.NODE_CONFIGURATION ADD COLUMN NODE_PROFILE VARCHAR(255) DEFAULT 'MIXED';

View File

@ -4,6 +4,7 @@ import com.google.inject.Inject;
import com.google.inject.name.Named;
import nu.marginalia.mq.persistence.MqPersistence;
import nu.marginalia.nodecfg.NodeConfigurationService;
import nu.marginalia.nodecfg.model.NodeProfile;
import nu.marginalia.storage.FileStorageService;
import nu.marginalia.storage.model.FileStorageBaseType;
import org.slf4j.Logger;
@ -56,7 +57,9 @@ public class NodeStatusWatcher {
private void setupNode() {
try {
configurationService.create(nodeId, "Node " + nodeId, true, false);
NodeProfile profile = NodeProfile.MIXED;
configurationService.create(nodeId, "Node " + nodeId, true, false, profile);
fileStorageService.createStorageBase("Index Data", Path.of("/idx"), nodeId, FileStorageBaseType.CURRENT);
fileStorageService.createStorageBase("Index Backups", Path.of("/backup"), nodeId, FileStorageBaseType.BACKUP);

View File

@ -182,4 +182,10 @@ public class ExecutorClient {
}
}
public void restartExecutorService(int node) {
channelPool.call(ExecutorApiBlockingStub::restartExecutorService)
.forNode(node)
.run(Empty.getDefaultInstance());
}
}

View File

@ -17,6 +17,8 @@ service ExecutorApi {
rpc downloadSampleData(RpcDownloadSampleData) returns (Empty) {}
rpc calculateAdjacencies(Empty) returns (Empty) {}
rpc restoreBackup(RpcFileStorageId) returns (Empty) {}
rpc restartExecutorService(Empty) returns (Empty) {}
}
service ExecutorCrawlApi {

View File

@ -1,31 +1,37 @@
package nu.marginalia.actor;
import nu.marginalia.nodecfg.model.NodeProfile;
import java.util.Set;
public enum ExecutorActor {
CRAWL,
LIVE_CRAWL,
RECRAWL,
RECRAWL_SINGLE_DOMAIN,
CONVERT_AND_LOAD,
PROC_CONVERTER_SPAWNER,
PROC_LOADER_SPAWNER,
PROC_CRAWLER_SPAWNER,
PROC_LIVE_CRAWL_SPAWNER,
MONITOR_PROCESS_LIVENESS,
MONITOR_FILE_STORAGE,
ADJACENCY_CALCULATION,
CRAWL_JOB_EXTRACTOR,
EXPORT_DATA,
EXPORT_SEGMENTATION_MODEL,
EXPORT_ATAGS,
EXPORT_TERM_FREQUENCIES,
EXPORT_FEEDS,
PROC_INDEX_CONSTRUCTOR_SPAWNER,
CONVERT,
RESTORE_BACKUP,
EXPORT_SAMPLE_DATA,
DOWNLOAD_SAMPLE,
SCRAPE_FEEDS,
UPDATE_RSS;
CRAWL(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
RECRAWL(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
RECRAWL_SINGLE_DOMAIN(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
PROC_CONVERTER_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
PROC_CRAWLER_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
ADJACENCY_CALCULATION(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
EXPORT_DATA(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
EXPORT_SEGMENTATION_MODEL(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
EXPORT_ATAGS(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
EXPORT_TERM_FREQUENCIES(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
EXPORT_FEEDS(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
EXPORT_SAMPLE_DATA(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
DOWNLOAD_SAMPLE(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
PROC_LOADER_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED, NodeProfile.SIDELOAD),
RESTORE_BACKUP(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED, NodeProfile.SIDELOAD),
CONVERT(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED, NodeProfile.SIDELOAD),
CONVERT_AND_LOAD(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED, NodeProfile.REALTIME, NodeProfile.SIDELOAD),
MONITOR_PROCESS_LIVENESS(NodeProfile.BATCH_CRAWL, NodeProfile.REALTIME, NodeProfile.MIXED, NodeProfile.SIDELOAD),
MONITOR_FILE_STORAGE(NodeProfile.BATCH_CRAWL, NodeProfile.REALTIME, NodeProfile.MIXED, NodeProfile.SIDELOAD),
PROC_INDEX_CONSTRUCTOR_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.REALTIME, NodeProfile.MIXED, NodeProfile.SIDELOAD),
LIVE_CRAWL(NodeProfile.REALTIME),
PROC_LIVE_CRAWL_SPAWNER(NodeProfile.REALTIME),
SCRAPE_FEEDS(NodeProfile.REALTIME),
UPDATE_RSS(NodeProfile.REALTIME);
public String id() {
return "fsm:" + name().toLowerCase();
@ -35,4 +41,9 @@ public enum ExecutorActor {
return "fsm:" + name().toLowerCase() + ":" + node;
}
ExecutorActor(NodeProfile... profileSet) {
this.profileSet = Set.of(profileSet);
}
public Set<NodeProfile> profileSet;
}

View File

@ -10,11 +10,14 @@ import nu.marginalia.actor.state.ActorStateInstance;
import nu.marginalia.actor.state.ActorStep;
import nu.marginalia.actor.task.*;
import nu.marginalia.mq.MessageQueueFactory;
import nu.marginalia.nodecfg.NodeConfigurationService;
import nu.marginalia.nodecfg.model.NodeConfiguration;
import nu.marginalia.service.control.ServiceEventLog;
import nu.marginalia.service.server.BaseServiceParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -28,10 +31,13 @@ public class ExecutorActorControlService {
public Map<ExecutorActor, ActorPrototype> actorDefinitions = new HashMap<>();
private final int node;
private final NodeConfiguration nodeConfiguration;
private final Logger logger = LoggerFactory.getLogger(getClass());
@Inject
public ExecutorActorControlService(MessageQueueFactory messageQueueFactory,
NodeConfigurationService configurationService,
BaseServiceParams baseServiceParams,
ConvertActor convertActor,
ConvertAndLoadActor convertAndLoadActor,
@ -56,12 +62,14 @@ public class ExecutorActorControlService {
DownloadSampleActor downloadSampleActor,
ScrapeFeedsActor scrapeFeedsActor,
ExecutorActorStateMachines stateMachines,
UpdateRssActor updateRssActor) {
UpdateRssActor updateRssActor) throws SQLException {
this.messageQueueFactory = messageQueueFactory;
this.eventLog = baseServiceParams.eventLog;
this.stateMachines = stateMachines;
this.node = baseServiceParams.configuration.node();
this.nodeConfiguration = configurationService.get(node);
register(ExecutorActor.CRAWL, crawlActor);
register(ExecutorActor.LIVE_CRAWL, liveCrawlActor);
register(ExecutorActor.RECRAWL_SINGLE_DOMAIN, recrawlSingleDomainActor);
@ -95,6 +103,11 @@ public class ExecutorActorControlService {
}
private void register(ExecutorActor process, RecordActorPrototype graph) {
if (!process.profileSet.contains(nodeConfiguration.profile())) {
return;
}
var sm = new ActorStateMachine(messageQueueFactory, process.id(), node, UUID.randomUUID(), graph);
sm.listen((function, param) -> logStateChange(process, function));

View File

@ -10,6 +10,8 @@ import nu.marginalia.actor.state.ActorResumeBehavior;
import nu.marginalia.actor.state.ActorStep;
import nu.marginalia.actor.state.Resume;
import nu.marginalia.model.EdgeDomain;
import nu.marginalia.nodecfg.NodeConfigurationService;
import nu.marginalia.nodecfg.model.NodeProfile;
import nu.marginalia.service.control.ServiceEventLog;
import nu.marginalia.service.module.ServiceConfiguration;
import org.jsoup.Jsoup;
@ -39,6 +41,7 @@ public class ScrapeFeedsActor extends RecordActorPrototype {
private final Duration pollInterval = Duration.ofHours(6);
private final ServiceEventLog eventLog;
private final NodeConfigurationService nodeConfigurationService;
private final HikariDataSource dataSource;
private final int nodeId;
@ -54,8 +57,8 @@ public class ScrapeFeedsActor extends RecordActorPrototype {
public ActorStep transition(ActorStep self) throws Exception {
return switch(self) {
case Initial() -> {
if (nodeId > 1) {
yield new End();
if (nodeConfigurationService.get(nodeId).profile() != NodeProfile.REALTIME) {
yield new Error("Invalid node profile for RSS update");
}
else {
yield new Wait(LocalDateTime.now().toString());
@ -177,10 +180,12 @@ public class ScrapeFeedsActor extends RecordActorPrototype {
public ScrapeFeedsActor(Gson gson,
ServiceEventLog eventLog,
ServiceConfiguration configuration,
NodeConfigurationService nodeConfigurationService,
HikariDataSource dataSource)
{
super(gson);
this.eventLog = eventLog;
this.nodeConfigurationService = nodeConfigurationService;
this.dataSource = dataSource;
this.nodeId = configuration.node();
}

View File

@ -11,6 +11,8 @@ import nu.marginalia.api.feeds.RpcFeedUpdateMode;
import nu.marginalia.mq.MqMessage;
import nu.marginalia.mq.MqMessageState;
import nu.marginalia.mq.persistence.MqPersistence;
import nu.marginalia.nodecfg.NodeConfigurationService;
import nu.marginalia.nodecfg.model.NodeProfile;
import nu.marginalia.service.module.ServiceConfiguration;
import java.time.Duration;
@ -25,13 +27,19 @@ public class UpdateRssActor extends RecordActorPrototype {
private final Duration updateInterval = Duration.ofHours(24);
private final int cleanInterval = 60;
private final NodeConfigurationService nodeConfigurationService;
private final MqPersistence persistence;
@Inject
public UpdateRssActor(Gson gson, FeedsClient feedsClient, ServiceConfiguration serviceConfiguration, MqPersistence persistence) {
public UpdateRssActor(Gson gson,
FeedsClient feedsClient,
ServiceConfiguration serviceConfiguration,
NodeConfigurationService nodeConfigurationService,
MqPersistence persistence) {
super(gson);
this.feedsClient = feedsClient;
this.nodeId = serviceConfiguration.node();
this.nodeConfigurationService = nodeConfigurationService;
this.persistence = persistence;
}
@ -55,9 +63,8 @@ public class UpdateRssActor extends RecordActorPrototype {
public ActorStep transition(ActorStep self) throws Exception {
return switch (self) {
case Initial() -> {
if (nodeId > 1) {
// Only run on the first node
yield new End();
if (nodeConfigurationService.get(nodeId).profile() != NodeProfile.REALTIME) {
yield new Error("Invalid node profile for RSS update");
}
else {
// Wait for 5 minutes before starting the first update, to give the system time to start up properly

View File

@ -15,9 +15,12 @@ import nu.marginalia.service.module.ServiceConfiguration;
import nu.marginalia.service.server.DiscoverableService;
import nu.marginalia.storage.FileStorageService;
import nu.marginalia.storage.model.FileStorageId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@ -32,6 +35,8 @@ public class ExecutorGrpcService
private final ServiceConfiguration serviceConfiguration;
private final ExecutorActorControlService actorControlService;
private static final Logger logger = LoggerFactory.getLogger(ExecutorGrpcService.class);
@Inject
public ExecutorGrpcService(ActorApi actorApi,
FileStorageService fileStorageService,
@ -240,5 +245,22 @@ public class ExecutorGrpcService
}
}
@Override
public void restartExecutorService(Empty request, StreamObserver<Empty> responseObserver) {
responseObserver.onNext(Empty.getDefaultInstance());
responseObserver.onCompleted();
logger.info("Restarting executor service on node {}", serviceConfiguration.node());
try {
// Wait for the response to be sent before restarting
Thread.sleep(Duration.ofSeconds(5));
}
catch (InterruptedException e) {
logger.warn("Interrupted while waiting for restart", e);
}
System.exit(0);
}
}

View File

@ -12,6 +12,7 @@ import nu.marginalia.control.sys.svc.HeartbeatService;
import nu.marginalia.executor.client.ExecutorClient;
import nu.marginalia.nodecfg.NodeConfigurationService;
import nu.marginalia.nodecfg.model.NodeConfiguration;
import nu.marginalia.nodecfg.model.NodeProfile;
import nu.marginalia.service.ServiceId;
import nu.marginalia.service.ServiceMonitors;
import nu.marginalia.storage.FileStorageService;
@ -52,7 +53,8 @@ public class ControlNodeService {
HikariDataSource dataSource,
ServiceMonitors monitors,
RedirectControl redirectControl,
NodeConfigurationService nodeConfigurationService, ControlCrawlDataService crawlDataService)
NodeConfigurationService nodeConfigurationService,
ControlCrawlDataService crawlDataService)
{
this.fileStorageService = fileStorageService;
this.rendererFactory = rendererFactory;
@ -269,6 +271,8 @@ public class ControlNodeService {
String act = request.queryParams("act");
if ("config".equals(act)) {
var oldConfig = nodeConfigurationService.get(nodeId);
var newConfig = new NodeConfiguration(
nodeId,
request.queryParams("description"),
@ -276,10 +280,19 @@ public class ControlNodeService {
"on".equalsIgnoreCase(request.queryParams("autoClean")),
"on".equalsIgnoreCase(request.queryParams("includeInPrecession")),
"on".equalsIgnoreCase(request.queryParams("keepWarcs")),
NodeProfile.valueOf(request.queryParams("profile")),
"on".equalsIgnoreCase(request.queryParams("disabled"))
);
nodeConfigurationService.save(newConfig);
if (!(Objects.equals(oldConfig.profile(), newConfig.profile()))) {
// Restart the executor service if the profile has changed
executorClient.restartExecutorService(nodeId);
}
else if (newConfig.disabled()) {
executorClient.restartExecutorService(nodeId);
}
}
else if ("storage".equals(act)) {
throw new UnsupportedOperationException();

View File

@ -21,6 +21,35 @@
<input class="form-control" type="text" name="description" value="{{config.description}}"/>
</div>
<div class="mb-5">
<label for="profile" class="form-label">Profile</label>
<select class="form-select" name="profile" id="profile">
<option value="BATCH_CRAWL" {{#if node.profile.batchCrawl}}selected{{/if}}>Batch Crawl</option>
<option value="SIDELOAD" {{#if node.profile.sideload}}selected{{/if}}>Sideload</option>
<option value="REALTIME" {{#if node.profile.realtime}}selected{{/if}}>Real Time</option>
<option value="MIXED" {{#if node.profile.mixed}}selected{{/if}}>Mixed Use</option>
</select>
<div class="form-text">The node profile configures which actors are available.
<ul class="my-1">
<li>
<strong>Batch Crawl</strong> - This node is configured for batch crawling. It will not have the sideload actors available.
</li>
<li>
<strong>Sideload</strong> - This node is configured for sideloading. It will not have the batch crawl actors available.
</li>
<li>
<strong>Real Time</strong> - This node is configured for real time processing.
It will not have the batch crawl or sideload actors available, but have actors for real time (daily) crawling.
</li>
<li>
<strong>Mixed Use</strong> - This node is configured for both batch crawling and sideloading.
</li>
</ul>
</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="acceptQueries" {{#if config.acceptQueries}}checked{{/if}}>
<label class="form-check-label" for="acceptQueries">Accept queries</label>

View File

@ -1,5 +1,5 @@
<h1 class="my-3">Index Node {{node.id}}</h1>
<h1 class="my-3">Index Node {{node.id}}: {{node.profile}}</h1>
{{#if node.disabled}}
<small class="text-danger">This index node is disabled!</small>
{{/if}}
@ -10,8 +10,10 @@
<li class="nav-item">
<a class="nav-link {{#if tab.overview}}active{{/if}}" href="/nodes/{{node.id}}/">Overview</a>
</li>
{{#unless node.profile.realtime}}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {{#if tab.actions}}active{{/if}}" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Actions</a>
{{#if node.profile.permitBatchCrawl}}
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=new-crawl">New Crawl</a></li>
<li><hr class="dropdown-divider"></li>
@ -19,11 +21,6 @@
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=load">Load Processed Data</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=repartition">Repartition Index</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-encyclopedia">Sideload Encyclopedia</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-stackexchange">Sideload Stackexchange</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-warc">Sideload WARC Files</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-dirtree">Sideload Dirtree</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-reddit">Sideload Reddit</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=download-sample-data">Download Sample Crawl Data</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=export-db-data">Export Database Data</a></li>
@ -33,14 +30,32 @@
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=restore-backup">Restore Index Backup</a></li>
</ul>
{{/if}}
{{#if node.profile.permitSideload}}
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-encyclopedia">Sideload Encyclopedia</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-stackexchange">Sideload Stackexchange</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-warc">Sideload WARC Files</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-dirtree">Sideload Dirtree</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=sideload-reddit">Sideload Reddit</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=load">Load Processed Data</a></li>
<li><a class="dropdown-item" href="/nodes/{{node.id}}/actions?view=restore-backup">Restore Index Backup</a></li>
</ul>
{{/if}}
</li>
{{/unless}}
<li class="nav-item">
<a class="nav-link {{#if tab.actors}}active{{/if}}" href="/nodes/{{node.id}}/actors">Actors</a>
</li>
{{#unless node.profile.realtime}}
<li class="nav-item">
<a class="nav-link {{#if tab.storage}}active{{/if}}" href="/nodes/{{node.id}}/storage/">Storage</a>
</li>
{{/unless}}
<li class="nav-item">
<a class="nav-link {{#if tab.config}}active{{/if}}" href="/nodes/{{node.id}}/configuration">Configuration</a>
</li>
</nav>

View File

@ -2,13 +2,16 @@
<h2>Nodes</h2>
<table class="table">
<tr>
<th>Node</th><th>Queries</th><th>Enabled</th><th>Index</th><th>Executor</th>
<th>Node</th><th>Profile</th><th>Queries</th><th>Enabled</th><th>Index</th><th>Executor</th>
</tr>
{{#each .}}
<tr>
<td>
<a href="/nodes/{{id}}">node-{{id}}</a>
</td>
<td>
{{configuration.profile}}
</td>
<td>
{{#if configuration.acceptQueries}}
&check;