mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-22 20:48:59 +00:00
(search) Remove Spark and migrate to Jooby for the search service
This commit is contained in:
parent
2fbf201761
commit
fdee07048d
@ -48,6 +48,7 @@ ext {
|
||||
dockerImageTag='latest'
|
||||
dockerImageRegistry='marginalia'
|
||||
jibVersion = '3.4.3'
|
||||
|
||||
}
|
||||
|
||||
idea {
|
||||
|
@ -42,6 +42,12 @@ dependencies {
|
||||
implementation libs.bundles.curator
|
||||
implementation libs.bundles.flyway
|
||||
|
||||
libs.bundles.jooby.get().each {
|
||||
implementation dependencies.create(it) {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
}
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
implementation libs.bundles.mariadb
|
||||
|
||||
|
@ -0,0 +1,174 @@
|
||||
package nu.marginalia.service.server;
|
||||
|
||||
import io.jooby.*;
|
||||
import io.prometheus.client.Counter;
|
||||
import nu.marginalia.mq.inbox.MqInboxIf;
|
||||
import nu.marginalia.service.client.ServiceNotAvailableException;
|
||||
import nu.marginalia.service.discovery.property.ServiceEndpoint;
|
||||
import nu.marginalia.service.discovery.property.ServiceKey;
|
||||
import nu.marginalia.service.discovery.property.ServicePartition;
|
||||
import nu.marginalia.service.module.ServiceConfiguration;
|
||||
import nu.marginalia.service.server.jte.JteModule;
|
||||
import nu.marginalia.service.server.mq.ServiceMqSubscription;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
public class JoobyService {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
// Marker for filtering out sensitive content from the persistent logs
|
||||
private final Marker httpMarker = MarkerFactory.getMarker("HTTP");
|
||||
|
||||
private final Initialization initialization;
|
||||
|
||||
private final static Counter request_counter = Counter.build("wmsa_request_counter", "Request Counter")
|
||||
.labelNames("service", "node")
|
||||
.register();
|
||||
private final static Counter request_counter_good = Counter.build("wmsa_request_counter_good", "Good Requests")
|
||||
.labelNames("service", "node")
|
||||
.register();
|
||||
private final static Counter request_counter_bad = Counter.build("wmsa_request_counter_bad", "Bad Requests")
|
||||
.labelNames("service", "node")
|
||||
.register();
|
||||
private final static Counter request_counter_err = Counter.build("wmsa_request_counter_err", "Error Requests")
|
||||
.labelNames("service", "node")
|
||||
.register();
|
||||
private final String serviceName;
|
||||
private static volatile boolean initialized = false;
|
||||
|
||||
protected final MqInboxIf messageQueueInbox;
|
||||
private final int node;
|
||||
private GrpcServer grpcServer;
|
||||
|
||||
private ServiceConfiguration config;
|
||||
private final List<MvcExtension> joobyServices;
|
||||
private final ServiceEndpoint restEndpoint;
|
||||
|
||||
public JoobyService(BaseServiceParams params,
|
||||
ServicePartition partition,
|
||||
List<DiscoverableService> grpcServices,
|
||||
List<MvcExtension> joobyServices
|
||||
) throws Exception {
|
||||
|
||||
this.joobyServices = joobyServices;
|
||||
this.initialization = params.initialization;
|
||||
config = params.configuration;
|
||||
node = config.node();
|
||||
|
||||
String inboxName = config.serviceName();
|
||||
logger.info("Inbox name: {}", inboxName);
|
||||
|
||||
var serviceRegistry = params.serviceRegistry;
|
||||
|
||||
restEndpoint = serviceRegistry.registerService(ServiceKey.forRest(config.serviceId(), config.node()),
|
||||
config.instanceUuid(), config.externalAddress());
|
||||
|
||||
var mqInboxFactory = params.messageQueueInboxFactory;
|
||||
messageQueueInbox = mqInboxFactory.createSynchronousInbox(inboxName, config.node(), config.instanceUuid());
|
||||
messageQueueInbox.subscribe(new ServiceMqSubscription(this));
|
||||
|
||||
serviceName = System.getProperty("service-name");
|
||||
|
||||
initialization.addCallback(params.heartbeat::start);
|
||||
initialization.addCallback(messageQueueInbox::start);
|
||||
initialization.addCallback(() -> params.eventLog.logEvent("SVC-INIT", serviceName + ":" + config.node()));
|
||||
initialization.addCallback(() -> serviceRegistry.announceInstance(config.instanceUuid()));
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
if (e instanceof ServiceNotAvailableException) {
|
||||
// reduce log spam for this common case
|
||||
logger.error("Service not available: {}", e.getMessage());
|
||||
}
|
||||
else {
|
||||
logger.error("Uncaught exception", e);
|
||||
}
|
||||
request_counter_err.labels(serviceName, Integer.toString(node)).inc();
|
||||
});
|
||||
|
||||
if (!initialization.isReady() && ! initialized ) {
|
||||
initialized = true;
|
||||
grpcServer = new GrpcServer(config, serviceRegistry, partition, grpcServices);
|
||||
grpcServer.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void startJooby(Jooby jooby) {
|
||||
|
||||
logger.info("{} Listening to {}:{} ({})", getClass().getSimpleName(),
|
||||
restEndpoint.host(),
|
||||
restEndpoint.port(),
|
||||
config.externalAddress());
|
||||
|
||||
jooby.install(new JteModule(Path.of("/app/resources/jte"), Path.of("/app/classes/jte-precompiled")));
|
||||
|
||||
var options = new ServerOptions();
|
||||
options.setHost(config.bindAddress());
|
||||
options.setPort(restEndpoint.port());
|
||||
jooby.setServerOptions(options);
|
||||
|
||||
jooby.get("/internal/ping", ctx -> "pong");
|
||||
jooby.get("/internal/started", this::isInitialized);
|
||||
jooby.get("/internal/ready", this::isReady);
|
||||
|
||||
for (var service : joobyServices) {
|
||||
jooby.mvc(service);
|
||||
}
|
||||
|
||||
jooby.assets("/webfonts/*", Paths.get("/app/resources/static/webfonts"))
|
||||
.setMaxAge(Duration.ofDays(365));
|
||||
jooby.assets("/*", Paths.get("/app/resources/static"));
|
||||
|
||||
jooby.before(this::auditRequestIn);
|
||||
jooby.after(this::auditRequestOut);
|
||||
}
|
||||
|
||||
private Object isInitialized(Context ctx) {
|
||||
if (initialization.isReady()) {
|
||||
return "ok";
|
||||
}
|
||||
else {
|
||||
ctx.setResponseCode(StatusCode.FAILED_DEPENDENCY_CODE);
|
||||
return "bad";
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private String isReady(Context ctx) {
|
||||
if (isReady()) {
|
||||
return "ok";
|
||||
}
|
||||
else {
|
||||
ctx.setResponseCode(StatusCode.FAILED_DEPENDENCY_CODE);
|
||||
return "bad";
|
||||
}
|
||||
}
|
||||
|
||||
private void auditRequestIn(Context ctx) {
|
||||
request_counter.labels(serviceName, Integer.toString(node)).inc();
|
||||
}
|
||||
|
||||
private void auditRequestOut(Context ctx, Object result, Throwable failure) {
|
||||
if (ctx.getResponseCode().value() < 400) {
|
||||
request_counter_good.labels(serviceName, Integer.toString(node)).inc();
|
||||
}
|
||||
else {
|
||||
request_counter_bad.labels(serviceName, Integer.toString(node)).inc();
|
||||
}
|
||||
|
||||
if (failure != null) {
|
||||
logger.error("Request failed " + ctx.getMethod() + " " + ctx.getRequestURL(), failure);
|
||||
request_counter_err.labels(serviceName, Integer.toString(node)).inc();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,7 +16,7 @@ import spark.Spark;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Service {
|
||||
public class SparkService {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
// Marker for filtering out sensitive content from the persistent logs
|
||||
@ -43,10 +43,10 @@ public class Service {
|
||||
private final int node;
|
||||
private GrpcServer grpcServer;
|
||||
|
||||
public Service(BaseServiceParams params,
|
||||
Runnable configureStaticFiles,
|
||||
ServicePartition partition,
|
||||
List<DiscoverableService> grpcServices) throws Exception {
|
||||
public SparkService(BaseServiceParams params,
|
||||
Runnable configureStaticFiles,
|
||||
ServicePartition partition,
|
||||
List<DiscoverableService> grpcServices) throws Exception {
|
||||
|
||||
this.initialization = params.initialization;
|
||||
var config = params.configuration;
|
||||
@ -126,18 +126,18 @@ public class Service {
|
||||
}
|
||||
}
|
||||
|
||||
public Service(BaseServiceParams params,
|
||||
ServicePartition partition,
|
||||
List<DiscoverableService> grpcServices) throws Exception {
|
||||
public SparkService(BaseServiceParams params,
|
||||
ServicePartition partition,
|
||||
List<DiscoverableService> grpcServices) throws Exception {
|
||||
this(params,
|
||||
Service::defaultSparkConfig,
|
||||
SparkService::defaultSparkConfig,
|
||||
partition,
|
||||
grpcServices);
|
||||
}
|
||||
|
||||
public Service(BaseServiceParams params) throws Exception {
|
||||
public SparkService(BaseServiceParams params) throws Exception {
|
||||
this(params,
|
||||
Service::defaultSparkConfig,
|
||||
SparkService::defaultSparkConfig,
|
||||
ServicePartition.any(),
|
||||
List.of());
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package nu.marginalia.service.server.jte;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.NonNull;
|
||||
import edu.umd.cs.findbugs.annotations.Nullable;
|
||||
import gg.jte.ContentType;
|
||||
import gg.jte.TemplateEngine;
|
||||
import gg.jte.resolve.DirectoryCodeResolver;
|
||||
import io.jooby.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
// Temporary workaround for a bug
|
||||
// APL-2.0 https://github.com/jooby-project/jooby
|
||||
public class JteModule implements Extension {
|
||||
private Path sourceDirectory;
|
||||
private Path classDirectory;
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
public JteModule(@NonNull Path sourceDirectory, @NonNull Path classDirectory) {
|
||||
this.sourceDirectory = (Path)Objects.requireNonNull(sourceDirectory, "Source directory is required.");
|
||||
this.classDirectory = (Path)Objects.requireNonNull(classDirectory, "Class directory is required.");
|
||||
}
|
||||
|
||||
public JteModule(@NonNull Path sourceDirectory) {
|
||||
this.sourceDirectory = (Path)Objects.requireNonNull(sourceDirectory, "Source directory is required.");
|
||||
}
|
||||
|
||||
public JteModule(@NonNull TemplateEngine templateEngine) {
|
||||
this.templateEngine = (TemplateEngine)Objects.requireNonNull(templateEngine, "Template engine is required.");
|
||||
}
|
||||
|
||||
public void install(@NonNull Jooby application) {
|
||||
if (this.templateEngine == null) {
|
||||
this.templateEngine = create(application.getEnvironment(), this.sourceDirectory, this.classDirectory);
|
||||
}
|
||||
|
||||
ServiceRegistry services = application.getServices();
|
||||
services.put(TemplateEngine.class, this.templateEngine);
|
||||
application.encoder(MediaType.html, new JteTemplateEngine(this.templateEngine));
|
||||
}
|
||||
|
||||
public static TemplateEngine create(@NonNull Environment environment, @NonNull Path sourceDirectory, @Nullable Path classDirectory) {
|
||||
boolean dev = environment.isActive("dev", new String[]{"test"});
|
||||
if (dev) {
|
||||
Objects.requireNonNull(sourceDirectory, "Source directory is required.");
|
||||
Path requiredClassDirectory = (Path)Optional.ofNullable(classDirectory).orElseGet(() -> sourceDirectory.resolve("jte-classes"));
|
||||
TemplateEngine engine = TemplateEngine.create(new DirectoryCodeResolver(sourceDirectory), requiredClassDirectory, ContentType.Html, environment.getClassLoader());
|
||||
Optional<List<String>> var10000 = Optional.ofNullable(System.getProperty("jooby.run.classpath")).map((it) -> it.split(File.pathSeparator)).map(Stream::of).map(Stream::toList);
|
||||
Objects.requireNonNull(engine);
|
||||
var10000.ifPresent(engine::setClassPath);
|
||||
return engine;
|
||||
} else {
|
||||
return classDirectory == null ? TemplateEngine.createPrecompiled(ContentType.Html) : TemplateEngine.createPrecompiled(classDirectory, ContentType.Html);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package nu.marginalia.service.server.jte;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.NonNull;
|
||||
import gg.jte.TemplateEngine;
|
||||
import io.jooby.Context;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import io.jooby.buffer.DataBuffer;
|
||||
import io.jooby.internal.jte.DataBufferOutput;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
// Temporary workaround for a bug
|
||||
// APL-2.0 https://github.com/jooby-project/jooby
|
||||
class JteTemplateEngine implements io.jooby.TemplateEngine {
|
||||
private final TemplateEngine jte;
|
||||
private final List<String> extensions;
|
||||
|
||||
public JteTemplateEngine(TemplateEngine jte) {
|
||||
this.jte = jte;
|
||||
this.extensions = List.of(".jte", ".kte");
|
||||
}
|
||||
|
||||
|
||||
@NonNull @Override
|
||||
public List<String> extensions() {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataBuffer render(Context ctx, ModelAndView modelAndView) {
|
||||
var buffer = ctx.getBufferFactory().allocateBuffer();
|
||||
var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8);
|
||||
var attributes = ctx.getAttributes();
|
||||
if (modelAndView instanceof MapModelAndView mapModelAndView) {
|
||||
var mapModel = new HashMap<String, Object>();
|
||||
mapModel.putAll(attributes);
|
||||
mapModel.putAll(mapModelAndView.getModel());
|
||||
jte.render(modelAndView.getView(), mapModel, output);
|
||||
} else {
|
||||
jte.render(modelAndView.getView(), modelAndView.getModel(), output);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package nu.marginalia.service.server.mq;
|
||||
import nu.marginalia.mq.MqMessage;
|
||||
import nu.marginalia.mq.inbox.MqInboxResponse;
|
||||
import nu.marginalia.mq.inbox.MqSubscription;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -15,10 +14,10 @@ import java.util.Map;
|
||||
public class ServiceMqSubscription implements MqSubscription {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ServiceMqSubscription.class);
|
||||
private final Map<String, Method> requests = new HashMap<>();
|
||||
private final Service service;
|
||||
private final Object service;
|
||||
|
||||
|
||||
public ServiceMqSubscription(Service service) {
|
||||
public ServiceMqSubscription(Object service) {
|
||||
this.service = service;
|
||||
|
||||
/* Wire up all methods annotated with @MqRequest and @MqNotification
|
||||
|
@ -11,7 +11,7 @@ import nu.marginalia.api.svc.RateLimiterService;
|
||||
import nu.marginalia.api.svc.ResponseCache;
|
||||
import nu.marginalia.model.gson.GsonFactory;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import nu.marginalia.service.server.mq.MqRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -21,7 +21,7 @@ import spark.Request;
|
||||
import spark.Response;
|
||||
import spark.Spark;
|
||||
|
||||
public class ApiService extends Service {
|
||||
public class ApiService extends SparkService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private final Gson gson = GsonFactory.get();
|
||||
|
@ -9,7 +9,7 @@ import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import nu.marginalia.screenshot.ScreenshotService;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
@ -18,7 +18,7 @@ import spark.Spark;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class DatingService extends Service {
|
||||
public class DatingService extends SparkService {
|
||||
private final DomainBlacklist blacklist;
|
||||
private final DbBrowseDomainsSimilarCosine browseSimilarCosine;
|
||||
private final DbBrowseDomainsRandom browseRandom;
|
||||
|
@ -5,7 +5,7 @@ import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import nu.marginalia.service.server.StaticResources;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import spark.Request;
|
||||
@ -15,7 +15,7 @@ import spark.Spark;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
|
||||
public class ExplorerService extends Service {
|
||||
public class ExplorerService extends SparkService {
|
||||
|
||||
private final MustacheRenderer<Object> renderer;
|
||||
private final HikariDataSource dataSource;
|
||||
|
@ -1,10 +1,8 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'io.freefair.sass-base' version '8.4'
|
||||
id 'io.freefair.sass-java' version '8.4'
|
||||
id 'application'
|
||||
id 'jvm-test-suite'
|
||||
|
||||
id 'gg.jte.gradle' version '3.1.15'
|
||||
id 'com.google.cloud.tools.jib' version '3.4.3'
|
||||
}
|
||||
|
||||
@ -15,18 +13,16 @@ application {
|
||||
|
||||
tasks.distZip.enabled = false
|
||||
|
||||
|
||||
jte {
|
||||
sourceDirectory = java.nio.file.Path.of('resources/jte')
|
||||
targetDirectory = java.nio.file.Path.of('build/classes/jte-precompiled')
|
||||
generate()
|
||||
}
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
|
||||
}
|
||||
}
|
||||
sass {
|
||||
sourceMapEnabled = true
|
||||
sourceMapEmbed = true
|
||||
outputStyle = EXPANDED
|
||||
}
|
||||
|
||||
|
||||
apply from: "$rootProject.projectDir/srcsets.gradle"
|
||||
apply from: "$rootProject.projectDir/docker.gradle"
|
||||
@ -64,18 +60,24 @@ dependencies {
|
||||
exclude group: 'com.google.guava'
|
||||
}
|
||||
implementation libs.handlebars
|
||||
implementation dependencies.create(libs.spark.get()) {
|
||||
exclude group: 'org.eclipse.jetty'
|
||||
}
|
||||
implementation libs.bundles.jetty
|
||||
implementation libs.opencsv
|
||||
implementation libs.trove
|
||||
implementation libs.jte
|
||||
|
||||
libs.bundles.jooby.get().each {
|
||||
implementation dependencies.create(it) {
|
||||
// Jooby pulls in an incompatible slf4j version that breaks all logs
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.fastutil
|
||||
implementation libs.bundles.gson
|
||||
implementation libs.bundles.mariadb
|
||||
implementation libs.bundles.nlp
|
||||
|
||||
annotationProcessor libs.jooby.apt
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
testImplementation libs.mockito
|
||||
@ -85,12 +87,14 @@ dependencies {
|
||||
testImplementation 'org.testcontainers:mariadb:1.17.4'
|
||||
testImplementation 'org.testcontainers:junit-jupiter:1.17.4'
|
||||
testImplementation project(':code:libraries:test-helpers')
|
||||
|
||||
testImplementation dependencies.create(libs.spark.get())
|
||||
}
|
||||
|
||||
task compileTailwind {
|
||||
def inputFile = file('tailwind/globals.css')
|
||||
def configFile = file('tailwind/tailwind.config.js')
|
||||
def outputFile = file('resources/static/search/css/style.css')
|
||||
def outputFile = file('resources/static/css/style.css')
|
||||
def jteDir = file('resources/jte')
|
||||
|
||||
inputs.file inputFile
|
||||
|
@ -3,15 +3,16 @@ package nu.marginalia.search;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Injector;
|
||||
import io.jooby.ExecutionMode;
|
||||
import io.jooby.Jooby;
|
||||
import nu.marginalia.service.MainClass;
|
||||
import nu.marginalia.service.discovery.ServiceRegistryIf;
|
||||
import nu.marginalia.service.module.ServiceConfiguration;
|
||||
import nu.marginalia.service.module.ServiceDiscoveryModule;
|
||||
import nu.marginalia.service.ServiceId;
|
||||
import nu.marginalia.service.module.ServiceConfigurationModule;
|
||||
import nu.marginalia.service.discovery.ServiceRegistryIf;
|
||||
import nu.marginalia.service.module.DatabaseModule;
|
||||
import nu.marginalia.service.module.ServiceConfiguration;
|
||||
import nu.marginalia.service.module.ServiceConfigurationModule;
|
||||
import nu.marginalia.service.module.ServiceDiscoveryModule;
|
||||
import nu.marginalia.service.server.Initialization;
|
||||
import spark.Spark;
|
||||
|
||||
public class SearchMain extends MainClass {
|
||||
private final SearchService service;
|
||||
@ -25,8 +26,6 @@ public class SearchMain extends MainClass {
|
||||
|
||||
init(ServiceId.Search, args);
|
||||
|
||||
Spark.staticFileLocation("/static/search/");
|
||||
|
||||
Injector injector = Guice.createInjector(
|
||||
new SearchModule(),
|
||||
new ServiceConfigurationModule(ServiceId.Search),
|
||||
@ -34,14 +33,22 @@ public class SearchMain extends MainClass {
|
||||
new DatabaseModule(false)
|
||||
);
|
||||
|
||||
|
||||
// Orchestrate the boot order for the services
|
||||
var registry = injector.getInstance(ServiceRegistryIf.class);
|
||||
var configuration = injector.getInstance(ServiceConfiguration.class);
|
||||
orchestrateBoot(registry, configuration);
|
||||
|
||||
injector.getInstance(SearchMain.class);
|
||||
var main = injector.getInstance(SearchMain.class);
|
||||
injector.getInstance(Initialization.class).setReady();
|
||||
|
||||
Jooby.runApp(new String[] { "application.env=prod" }, ExecutionMode.WORKER, () -> new Jooby() {
|
||||
{
|
||||
main.start(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void start(Jooby jooby) {
|
||||
service.startJooby(jooby);
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,16 @@ import io.prometheus.client.Counter;
|
||||
import io.prometheus.client.Histogram;
|
||||
import nu.marginalia.WebsiteUrl;
|
||||
import nu.marginalia.search.svc.*;
|
||||
import nu.marginalia.service.discovery.property.ServicePartition;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.JoobyService;
|
||||
import nu.marginalia.service.server.StaticResources;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
import spark.Route;
|
||||
import spark.Spark;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchService extends Service {
|
||||
public class SearchService extends JoobyService {
|
||||
|
||||
private final WebsiteUrl websiteUrl;
|
||||
private final StaticResources staticResources;
|
||||
@ -41,97 +37,103 @@ public class SearchService extends Service {
|
||||
WebsiteUrl websiteUrl,
|
||||
StaticResources staticResources,
|
||||
SearchFrontPageService frontPageService,
|
||||
SearchErrorPageService errorPageService,
|
||||
SearchAddToCrawlQueueService addToCrawlQueueService,
|
||||
SearchSiteInfoService siteInfoService,
|
||||
SearchCrosstalkService crosstalkService,
|
||||
SearchBrowseService searchBrowseService,
|
||||
SearchQueryService searchQueryService)
|
||||
throws Exception
|
||||
{
|
||||
super(params);
|
||||
throws Exception {
|
||||
super(params,
|
||||
ServicePartition.any(),
|
||||
List.of(),
|
||||
List.of(new SearchFrontPageService_(frontPageService),
|
||||
new SearchQueryService_(searchQueryService),
|
||||
new SearchSiteInfoService_(siteInfoService),
|
||||
new SearchCrosstalkService_(crosstalkService),
|
||||
new SearchAddToCrawlQueueService_(addToCrawlQueueService),
|
||||
new SearchBrowseService_(searchBrowseService)
|
||||
));
|
||||
|
||||
this.websiteUrl = websiteUrl;
|
||||
this.staticResources = staticResources;
|
||||
|
||||
Spark.staticFiles.expireTime(600);
|
||||
|
||||
SearchServiceMetrics.get("/search", searchQueryService::pathSearch);
|
||||
SearchServiceMetrics.get("/", frontPageService::render);
|
||||
SearchServiceMetrics.get("/news.xml", frontPageService::renderNewsFeed);
|
||||
|
||||
SearchServiceMetrics.post("/site/suggest/", addToCrawlQueueService::suggestCrawling);
|
||||
|
||||
SearchServiceMetrics.get("/site-search/:site/*", this::siteSearchRedir);
|
||||
|
||||
SearchServiceMetrics.get("/site", siteInfoService::handleOverview);
|
||||
SearchServiceMetrics.get("/site/:site", siteInfoService::handle);
|
||||
SearchServiceMetrics.post("/site/:site", siteInfoService::handlePost);
|
||||
|
||||
SearchServiceMetrics.get("/explore", searchBrowseService::handleBrowseRandom);
|
||||
SearchServiceMetrics.get("/explore/:site", searchBrowseService::handleBrowseSite);
|
||||
|
||||
SearchServiceMetrics.get("/crosstalk/", crosstalkService::handle);
|
||||
|
||||
SearchServiceMetrics.get("/:resource", this::serveStatic);
|
||||
Spark.exception(Exception.class, (e,p,q) -> {
|
||||
logger.error("Error during processing", e);
|
||||
wmsa_search_service_error_count.labels(p.pathInfo(), p.requestMethod()).inc();
|
||||
errorPageService.serveError(p, q);
|
||||
});
|
||||
|
||||
// Add compression
|
||||
Spark.after((rq, rs) -> {
|
||||
rs.header("Content-Encoding", "gzip");
|
||||
});
|
||||
|
||||
Spark.awaitInitialization();
|
||||
}
|
||||
//
|
||||
// SearchServiceMetrics.get("/search", searchQueryService::pathSearch);
|
||||
// SearchServiceMetrics.get("/", frontPageService::render);
|
||||
// SearchServiceMetrics.get("/news.xml", frontPageService::renderNewsFeed);
|
||||
//
|
||||
// SearchServiceMetrics.post("/site/suggest/", addToCrawlQueueService::suggestCrawling);
|
||||
//
|
||||
// SearchServiceMetrics.get("/site-search/:site/*", this::siteSearchRedir);
|
||||
//
|
||||
// SearchServiceMetrics.get("/site", siteInfoService::handleOverview);
|
||||
// SearchServiceMetrics.get("/site/:site", siteInfoService::handle);
|
||||
// SearchServiceMetrics.post("/site/:site", siteInfoService::handlePost);
|
||||
//
|
||||
// SearchServiceMetrics.get("/explore", searchBrowseService::handleBrowseRandom);
|
||||
// SearchServiceMetrics.get("/explore/:site", searchBrowseService::handleBrowseSite);
|
||||
//
|
||||
// SearchServiceMetrics.get("/crosstalk/", crosstalkService::handle);
|
||||
//
|
||||
// SearchServiceMetrics.get("/:resource", this::serveStatic);
|
||||
// Spark.exception(Exception.class, (e,p,q) -> {
|
||||
// logger.error("Error during processing", e);
|
||||
// wmsa_search_service_error_count.labels(p.pathInfo(), p.requestMethod()).inc();
|
||||
// errorPageService.serveError(p, q);
|
||||
// });
|
||||
//
|
||||
// // Add compression
|
||||
// Spark.after((rq, rs) -> {
|
||||
// rs.header("Content-Encoding", "gzip");
|
||||
// });
|
||||
//
|
||||
// Spark.awaitInitialization();
|
||||
|
||||
|
||||
|
||||
/** Wraps a route with a timer and a counter */
|
||||
private static class SearchServiceMetrics implements Route {
|
||||
private final Route delegatedRoute;
|
||||
|
||||
static void get(String path, Route route) {
|
||||
Spark.get(path, new SearchServiceMetrics(route));
|
||||
}
|
||||
static void post(String path, Route route) {
|
||||
Spark.post(path, new SearchServiceMetrics(route));
|
||||
}
|
||||
|
||||
private SearchServiceMetrics(Route delegatedRoute) {
|
||||
this.delegatedRoute = delegatedRoute;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handle(Request request, Response response) throws Exception {
|
||||
return wmsa_search_service_request_time
|
||||
.labels(request.matchedPath(), request.requestMethod())
|
||||
.time(() -> delegatedRoute.handle(request, response));
|
||||
}
|
||||
}
|
||||
|
||||
private Object serveStatic(Request request, Response response) {
|
||||
String resource = request.params("resource");
|
||||
staticResources.serveStatic("search", resource, request, response);
|
||||
return "";
|
||||
}
|
||||
|
||||
private Object siteSearchRedir(Request request, Response response) {
|
||||
final String site = request.params("site");
|
||||
final String searchTerms;
|
||||
|
||||
if (request.splat().length == 0) searchTerms = "";
|
||||
else searchTerms = request.splat()[0];
|
||||
|
||||
final String query = URLEncoder.encode(String.format("%s site:%s", searchTerms, site), StandardCharsets.UTF_8).trim();
|
||||
final String profile = request.queryParamOrDefault("profile", "yolo");
|
||||
|
||||
response.redirect(websiteUrl.withPath("search?query="+query+"&profile="+profile));
|
||||
|
||||
return "";
|
||||
}
|
||||
//
|
||||
//
|
||||
// /** Wraps a route with a timer and a counter */
|
||||
// private static class SearchServiceMetrics implements Route {
|
||||
// private final Route delegatedRoute;
|
||||
//
|
||||
// static void get(String path, Route route) {
|
||||
// Spark.get(path, new SearchServiceMetrics(route));
|
||||
// }
|
||||
// static void post(String path, Route route) {
|
||||
// Spark.post(path, new SearchServiceMetrics(route));
|
||||
// }
|
||||
//
|
||||
// private SearchServiceMetrics(Route delegatedRoute) {
|
||||
// this.delegatedRoute = delegatedRoute;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Object handle(Request request, Response response) throws Exception {
|
||||
// return wmsa_search_service_request_time
|
||||
// .labels(request.matchedPath(), request.requestMethod())
|
||||
// .time(() -> delegatedRoute.handle(request, response));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private Object serveStatic(Request request, Response response) {
|
||||
// String resource = request.params("resource");
|
||||
// staticResources.serveStatic("search", resource, request, response);
|
||||
// return "";
|
||||
// }
|
||||
//
|
||||
// private Object siteSearchRedir(Request request, Response response) {
|
||||
// final String site = request.params("site");
|
||||
// final String searchTerms;
|
||||
//
|
||||
// if (request.splat().length == 0) searchTerms = "";
|
||||
// else searchTerms = request.splat()[0];
|
||||
//
|
||||
// final String query = URLEncoder.encode(String.format("%s site:%s", searchTerms, site), StandardCharsets.UTF_8).trim();
|
||||
// final String profile = request.queryParamOrDefault("profile", "yolo");
|
||||
//
|
||||
// response.redirect(websiteUrl.withPath("search?query="+query+"&profile="+profile));
|
||||
//
|
||||
// return "";
|
||||
// }
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package nu.marginalia.search.command;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.search.command.commands.*;
|
||||
import spark.Response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -30,14 +30,14 @@ public class CommandEvaluator {
|
||||
defaultCommand = search;
|
||||
}
|
||||
|
||||
public Object eval(Response response, SearchParameters parameters) {
|
||||
public ModelAndView<?> eval(SearchParameters parameters) throws Exception {
|
||||
for (var cmd : specialCommands) {
|
||||
var maybe = cmd.process(response, parameters);
|
||||
var maybe = cmd.process(parameters);
|
||||
if (maybe.isPresent())
|
||||
return maybe.get();
|
||||
}
|
||||
|
||||
return defaultCommand.process(response, parameters).orElse("");
|
||||
return defaultCommand.process(parameters).orElseThrow();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
package nu.marginalia.search.command;
|
||||
|
||||
|
||||
import spark.Response;
|
||||
import io.jooby.ModelAndView;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SearchCommandInterface {
|
||||
Optional<Object> process(Response response, SearchParameters parameters);
|
||||
Optional<ModelAndView<?>> process(SearchParameters parameters) throws Exception;
|
||||
}
|
||||
|
@ -6,11 +6,9 @@ import nu.marginalia.index.query.limit.QueryStrategy;
|
||||
import nu.marginalia.index.query.limit.SpecificationLimit;
|
||||
import nu.marginalia.model.EdgeDomain;
|
||||
import nu.marginalia.search.model.SearchProfile;
|
||||
import spark.Request;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import static nu.marginalia.search.command.SearchRecentParameter.RECENT;
|
||||
@ -38,19 +36,6 @@ public record SearchParameters(WebsiteUrl url,
|
||||
false,
|
||||
page);
|
||||
}
|
||||
public static SearchParameters forRequest(String queryString, WebsiteUrl url, Request request) {
|
||||
return new SearchParameters(
|
||||
url,
|
||||
queryString,
|
||||
SearchProfile.getSearchProfile(request.queryParams("profile")),
|
||||
SearchJsParameter.parse(request.queryParams("js")),
|
||||
SearchRecentParameter.parse(request.queryParams("recent")),
|
||||
SearchTitleParameter.parse(request.queryParams("searchTitle")),
|
||||
SearchAdtechParameter.parse(request.queryParams("adtech")),
|
||||
"true".equals(request.queryParams("newfilter")),
|
||||
Integer.parseInt(Objects.requireNonNullElse(request.queryParams("page"), "1"))
|
||||
);
|
||||
}
|
||||
|
||||
public String profileStr() {
|
||||
return profile.filterId;
|
||||
|
@ -1,10 +1,10 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.exceptions.RedirectException;
|
||||
import spark.Response;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -24,7 +24,7 @@ public class BangCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Response response, SearchParameters parameters) {
|
||||
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
|
||||
|
||||
for (var entry : bangsToPattern.entrySet()) {
|
||||
String bangPattern = entry.getKey();
|
||||
@ -34,7 +34,7 @@ public class BangCommand implements SearchCommandInterface {
|
||||
|
||||
if (match.isPresent()) {
|
||||
var url = String.format(redirectPattern, URLEncoder.encode(match.get(), StandardCharsets.UTF_8));
|
||||
throw new RedirectException(url);
|
||||
new MapModelAndView("redirect.jte", Map.of("url", url));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import spark.Response;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
@ -19,7 +21,7 @@ public class BrowseRedirectCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Response response, SearchParameters parameters) {
|
||||
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
|
||||
if (!queryPatternPredicate.test(parameters.query())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@ -35,13 +37,9 @@ public class BrowseRedirectCommand implements SearchCommandInterface {
|
||||
redirectPath = "/explore/" + word;
|
||||
}
|
||||
|
||||
return Optional.of("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; %s">
|
||||
""".formatted(redirectPath));
|
||||
return Optional.of(
|
||||
new MapModelAndView("/redirect.jte", Map.of("url", redirectPath))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.search.JteRenderer;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.model.NavbarModel;
|
||||
import nu.marginalia.search.svc.SearchUnitConversionService;
|
||||
import spark.Response;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -24,9 +25,10 @@ public class ConvertCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Response response, SearchParameters parameters) {
|
||||
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
|
||||
var conversion = searchUnitConversionService.tryConversion(parameters.query());
|
||||
return conversion.map(s -> renderer.render("serp/unit-conversion.jte", Map.of(
|
||||
return conversion.map(s -> new MapModelAndView("serp/unit-conversion.jte",
|
||||
Map.of(
|
||||
"parameters", parameters,
|
||||
"navbar", NavbarModel.SEARCH,
|
||||
"result", s)
|
||||
|
@ -2,6 +2,8 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.api.math.MathClient;
|
||||
import nu.marginalia.api.math.model.DictionaryResponse;
|
||||
import nu.marginalia.search.JteRenderer;
|
||||
@ -10,7 +12,6 @@ import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.model.NavbarModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -35,14 +36,14 @@ public class DefinitionCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Response response, SearchParameters parameters) {
|
||||
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
|
||||
if (!queryPatternPredicate.test(parameters.query())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
DictionaryResponse result = lookupDefinition(parameters.query());
|
||||
|
||||
return Optional.of(renderer.render("serp/dict-lookup.jte",
|
||||
return Optional.of(new MapModelAndView("serp/dict-lookup.jte",
|
||||
Map.of("parameters", parameters,
|
||||
"result", result,
|
||||
"navbar", NavbarModel.SEARCH)
|
||||
|
@ -1,13 +1,14 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.search.JteRenderer;
|
||||
import nu.marginalia.search.SearchOperator;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.model.DecoratedSearchResults;
|
||||
import nu.marginalia.search.model.NavbarModel;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
@ -26,16 +27,10 @@ public class SearchCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Response response, SearchParameters parameters) {
|
||||
try {
|
||||
DecoratedSearchResults results = searchOperator.doSearch(parameters);
|
||||
return Optional.of(jteRenderer.render("serp/main.jte",
|
||||
Map.of("results", results, "navbar", NavbarModel.SEARCH)
|
||||
));
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return Optional.empty();
|
||||
}
|
||||
public Optional<ModelAndView<?>> process(SearchParameters parameters) throws InterruptedException {
|
||||
DecoratedSearchResults results = searchOperator.doSearch(parameters);
|
||||
return Optional.of(new MapModelAndView("serp/main.jte",
|
||||
Map.of("results", results, "navbar", NavbarModel.SEARCH)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
@ -22,7 +24,7 @@ public class SiteRedirectCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Response response, SearchParameters parameters) {
|
||||
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
|
||||
if (!queryPatternPredicate.test(parameters.query())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@ -37,14 +39,8 @@ public class SiteRedirectCommand implements SearchCommandInterface {
|
||||
default -> "info";
|
||||
};
|
||||
|
||||
return Optional.of("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; url=/site/%s?view=%s">
|
||||
""".formatted(domain, view)
|
||||
);
|
||||
String url = "/site/%s?view=%s".formatted(domain, view);
|
||||
return Optional.of(new MapModelAndView("/redirect.jte", Map.of("url", url)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
package nu.marginalia.search.exceptions;
|
||||
|
||||
public class RedirectException extends RuntimeException {
|
||||
public final String newUrl;
|
||||
|
||||
public RedirectException(String newUrl) {
|
||||
this.newUrl = newUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StackTraceElement[] getStackTrace() {
|
||||
return new StackTraceElement[0];
|
||||
}
|
||||
}
|
@ -2,40 +2,41 @@ package nu.marginalia.search.svc;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.WebsiteUrl;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import io.jooby.annotation.FormParam;
|
||||
import io.jooby.annotation.POST;
|
||||
import io.jooby.annotation.Path;
|
||||
import nu.marginalia.db.DbDomainQueries;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
import spark.Spark;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
public class SearchAddToCrawlQueueService {
|
||||
|
||||
private final DbDomainQueries domainQueries;
|
||||
private final WebsiteUrl websiteUrl;
|
||||
private final HikariDataSource dataSource;
|
||||
private final Logger logger = LoggerFactory.getLogger(SearchAddToCrawlQueueService.class);
|
||||
|
||||
@Inject
|
||||
public SearchAddToCrawlQueueService(DbDomainQueries domainQueries,
|
||||
WebsiteUrl websiteUrl,
|
||||
HikariDataSource dataSource) {
|
||||
this.domainQueries = domainQueries;
|
||||
this.websiteUrl = websiteUrl;
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public Object suggestCrawling(Request request, Response response) throws SQLException {
|
||||
logger.info("{}", request.queryParams());
|
||||
int id = Integer.parseInt(request.queryParams("id"));
|
||||
boolean nomisclick = "on".equals(request.queryParams("nomisclick"));
|
||||
@POST
|
||||
@Path("/site/suggest/")
|
||||
public ModelAndView<?> suggestCrawling(
|
||||
@FormParam int id,
|
||||
@FormParam String nomisclick
|
||||
) throws SQLException {
|
||||
|
||||
String domainName = getDomainName(id);
|
||||
|
||||
if (nomisclick) {
|
||||
if ("on".equals(nomisclick)) {
|
||||
logger.info("Adding {} to crawl queue", domainName);
|
||||
addToCrawlQueue(id);
|
||||
}
|
||||
@ -43,9 +44,7 @@ public class SearchAddToCrawlQueueService {
|
||||
logger.info("Nomisclick not set, not adding {} to crawl queue", domainName);
|
||||
}
|
||||
|
||||
response.redirect(websiteUrl.withPath("/site/" + domainName));
|
||||
|
||||
return "";
|
||||
return new MapModelAndView("/redirect.jte", Map.of("url", "/site/"+domainName));
|
||||
}
|
||||
|
||||
private void addToCrawlQueue(int id) throws SQLException {
|
||||
@ -62,7 +61,7 @@ public class SearchAddToCrawlQueueService {
|
||||
private String getDomainName(int id) {
|
||||
var domain = domainQueries.getDomain(id);
|
||||
if (domain.isEmpty())
|
||||
Spark.halt(404);
|
||||
throw new IllegalArgumentException();
|
||||
return domain.get().toString();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
package nu.marginalia.search.svc;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import io.jooby.annotation.GET;
|
||||
import io.jooby.annotation.Path;
|
||||
import io.jooby.annotation.PathParam;
|
||||
import nu.marginalia.api.domains.DomainInfoClient;
|
||||
import nu.marginalia.api.domains.model.SimilarDomain;
|
||||
import nu.marginalia.browse.DbBrowseDomainsRandom;
|
||||
@ -12,10 +17,7 @@ import nu.marginalia.model.EdgeDomain;
|
||||
import nu.marginalia.search.JteRenderer;
|
||||
import nu.marginalia.search.model.NavbarModel;
|
||||
import nu.marginalia.search.results.BrowseResultCleaner;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -47,16 +49,19 @@ public class SearchBrowseService {
|
||||
this.browseResultCleaner = browseResultCleaner;
|
||||
}
|
||||
|
||||
public String handleBrowseRandom(Request request, Response response) throws IOException {
|
||||
return jteRenderer.render("explore/main.jte",
|
||||
@GET
|
||||
@Path("/explore")
|
||||
public ModelAndView<?> handleBrowseRandom() {
|
||||
return new MapModelAndView("explore/main.jte",
|
||||
Map.of("navbar", NavbarModel.EXPLORE,
|
||||
"results", getRandomEntries(1)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public String handleBrowseSite(Request request, Response response) throws Exception {
|
||||
String domainName = request.params("site");
|
||||
@GET
|
||||
@Path("/explore/{domainName}")
|
||||
public ModelAndView<?> handleBrowseSite(@PathParam String domainName) throws Exception {
|
||||
BrowseResultSet entries;
|
||||
|
||||
try {
|
||||
@ -66,7 +71,7 @@ public class SearchBrowseService {
|
||||
entries = new BrowseResultSet(List.of(), domainName);
|
||||
}
|
||||
|
||||
return jteRenderer.render("explore/main.jte",
|
||||
return new MapModelAndView("explore/main.jte",
|
||||
Map.of("navbar", NavbarModel.EXPLORE,
|
||||
"results", entries
|
||||
)
|
||||
|
@ -1,6 +1,11 @@
|
||||
package nu.marginalia.search.svc;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import io.jooby.annotation.GET;
|
||||
import io.jooby.annotation.Path;
|
||||
import io.jooby.annotation.QueryParam;
|
||||
import nu.marginalia.search.JteRenderer;
|
||||
import nu.marginalia.search.SearchOperator;
|
||||
import nu.marginalia.search.model.NavbarModel;
|
||||
@ -9,8 +14,6 @@ import nu.marginalia.search.model.UrlDetails;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
@ -29,16 +32,15 @@ public class SearchCrosstalkService {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
public Object handle(Request request, Response response) throws SQLException {
|
||||
String domains = request.queryParams("domains");
|
||||
@GET
|
||||
@Path("/crosstalk")
|
||||
public ModelAndView<?> crosstalk(@QueryParam String domains) throws SQLException {
|
||||
String[] parts = StringUtils.split(domains, ',');
|
||||
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Expected exactly two domains");
|
||||
}
|
||||
|
||||
response.type("text/html");
|
||||
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
parts[i] = parts[i].trim();
|
||||
}
|
||||
@ -48,7 +50,7 @@ public class SearchCrosstalkService {
|
||||
|
||||
CrosstalkResult model = new CrosstalkResult(parts[0], parts[1], resAtoB.results, resBtoA.results);
|
||||
|
||||
return renderer.render(
|
||||
return new MapModelAndView(
|
||||
"siteinfo/crosstalk.jte",
|
||||
Map.of("model", model,
|
||||
"navbar", NavbarModel.SITEINFO));
|
||||
|
@ -1,41 +1,28 @@
|
||||
package nu.marginalia.search.svc;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import nu.marginalia.WebsiteUrl;
|
||||
import nu.marginalia.search.JteRenderer;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.model.NavbarModel;
|
||||
import nu.marginalia.search.model.SearchErrorMessageModel;
|
||||
import nu.marginalia.search.model.SearchFilters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
public class SearchErrorPageService {
|
||||
private final WebsiteUrl websiteUrl;
|
||||
private final JteRenderer jteRenderer;
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Inject
|
||||
public SearchErrorPageService(WebsiteUrl websiteUrl,
|
||||
JteRenderer jteRenderer) throws IOException {
|
||||
this.websiteUrl = websiteUrl;
|
||||
this.jteRenderer = jteRenderer;
|
||||
public SearchErrorPageService() throws IOException {
|
||||
}
|
||||
|
||||
public void serveError(Request request, Response rsp) {
|
||||
public ModelAndView<?> serveError(SearchParameters parameters) {
|
||||
|
||||
var params = SearchParameters.forRequest(
|
||||
request.queryParamOrDefault("query", ""),
|
||||
websiteUrl,
|
||||
request);
|
||||
|
||||
|
||||
rsp.body(jteRenderer.render("serp/error.jte",
|
||||
return new MapModelAndView("serp/error.jte",
|
||||
Map.of("navbar", NavbarModel.LIMBO,
|
||||
"model", new SearchErrorMessageModel(
|
||||
"An error occurred when communicating with the search engine index.",
|
||||
@ -44,11 +31,11 @@ public class SearchErrorPageService {
|
||||
an upgrade. The index typically takes a about two or three minutes
|
||||
to reload from a cold restart. Thanks for your patience.
|
||||
""",
|
||||
params,
|
||||
new SearchFilters(params)
|
||||
parameters,
|
||||
new SearchFilters(parameters)
|
||||
)
|
||||
)
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,30 +3,24 @@ package nu.marginalia.search.svc;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.annotation.GET;
|
||||
import io.jooby.annotation.Path;
|
||||
import nu.marginalia.WebsiteUrl;
|
||||
import nu.marginalia.search.JteRenderer;
|
||||
import nu.marginalia.search.model.NavbarModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Renders the front page (index) */
|
||||
@Singleton
|
||||
public class SearchFrontPageService {
|
||||
|
||||
private final HikariDataSource dataSource;
|
||||
private final JteRenderer jteRenderer;
|
||||
private final SearchQueryCountService searchVisitorCount;
|
||||
private final WebsiteUrl websiteUrl;
|
||||
|
||||
@ -34,21 +28,20 @@ public class SearchFrontPageService {
|
||||
|
||||
@Inject
|
||||
public SearchFrontPageService(HikariDataSource dataSource,
|
||||
JteRenderer jteRenderer,
|
||||
SearchQueryCountService searchVisitorCount, WebsiteUrl websiteUrl
|
||||
) throws IOException {
|
||||
SearchQueryCountService searchVisitorCount,
|
||||
WebsiteUrl websiteUrl
|
||||
) {
|
||||
this.dataSource = dataSource;
|
||||
this.jteRenderer = jteRenderer;
|
||||
this.searchVisitorCount = searchVisitorCount;
|
||||
this.websiteUrl = websiteUrl;
|
||||
}
|
||||
|
||||
public String render(Request request, Response response) {
|
||||
response.header("Cache-control", "public,max-age=3600");
|
||||
|
||||
return jteRenderer.render("serp/first.jte",
|
||||
Map.of("navbar", NavbarModel.SEARCH, "websiteUrl", websiteUrl)
|
||||
);
|
||||
@GET
|
||||
@Path("/")
|
||||
public MapModelAndView render() {
|
||||
return new MapModelAndView("serp/first.jte")
|
||||
.put("navbar", NavbarModel.SEARCH)
|
||||
.put("websiteUrl", websiteUrl);
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +70,7 @@ public class SearchFrontPageService {
|
||||
return items;
|
||||
}
|
||||
|
||||
/* FIXME
|
||||
public Object renderNewsFeed(Request request, Response response) {
|
||||
List<NewsItem> newsItems = getNewsItems();
|
||||
|
||||
@ -112,7 +106,7 @@ public class SearchFrontPageService {
|
||||
response.type("application/rss+xml");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}*/
|
||||
|
||||
private record IndexModel(List<NewsItem> news, int searchPerMinute) { }
|
||||
private record NewsItem(String title, String url, String source, LocalDate date) {}
|
||||
|
@ -1,14 +1,17 @@
|
||||
package nu.marginalia.search.svc;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.jooby.ModelAndView;
|
||||
import io.jooby.annotation.GET;
|
||||
import io.jooby.annotation.Path;
|
||||
import io.jooby.annotation.QueryParam;
|
||||
import nu.marginalia.WebsiteUrl;
|
||||
import nu.marginalia.search.command.CommandEvaluator;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.exceptions.RedirectException;
|
||||
import nu.marginalia.search.command.*;
|
||||
import nu.marginalia.search.model.SearchProfile;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class SearchQueryService {
|
||||
|
||||
@ -27,36 +30,34 @@ public class SearchQueryService {
|
||||
this.searchCommandEvaulator = searchCommandEvaulator;
|
||||
}
|
||||
|
||||
public Object pathSearch(Request request, Response response) {
|
||||
@GET
|
||||
@Path("/search")
|
||||
public ModelAndView<?> pathSearch(
|
||||
@QueryParam String query,
|
||||
@QueryParam String profile,
|
||||
@QueryParam String js,
|
||||
@QueryParam String recent,
|
||||
@QueryParam String title,
|
||||
@QueryParam String adtech,
|
||||
@QueryParam Integer page
|
||||
) {
|
||||
try {
|
||||
return searchCommandEvaulator.eval(response, parseParameters(request));
|
||||
}
|
||||
catch (RedirectException ex) {
|
||||
response.redirect(ex.newUrl);
|
||||
SearchParameters parameters = new SearchParameters(websiteUrl,
|
||||
query,
|
||||
SearchProfile.getSearchProfile(profile),
|
||||
SearchJsParameter.parse(js),
|
||||
SearchRecentParameter.parse(recent),
|
||||
SearchTitleParameter.parse(title),
|
||||
SearchAdtechParameter.parse(adtech),
|
||||
false,
|
||||
Objects.requireNonNullElse(page,1));
|
||||
|
||||
return searchCommandEvaulator.eval(parameters);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.error("Error", ex);
|
||||
errorPageService.serveError(request, response);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private SearchParameters parseParameters(Request request) {
|
||||
try {
|
||||
final String queryParam = request.queryParams("query");
|
||||
|
||||
if (null == queryParam || queryParam.isBlank()) {
|
||||
throw new RedirectException(websiteUrl.url());
|
||||
}
|
||||
|
||||
return SearchParameters.forRequest(queryParam.trim(), websiteUrl, request);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Bots keep sending bad requests, suppress the error otherwise it will
|
||||
// fill up the logs.
|
||||
|
||||
throw new RedirectException(websiteUrl.url());
|
||||
return errorPageService.serveError(SearchParameters.defaultsForQuery(websiteUrl, query, page));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ package nu.marginalia.search.svc;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import io.jooby.MapModelAndView;
|
||||
import io.jooby.ModelAndView;
|
||||
import io.jooby.annotation.*;
|
||||
import nu.marginalia.api.domains.DomainInfoClient;
|
||||
import nu.marginalia.api.domains.model.DomainInformation;
|
||||
import nu.marginalia.api.domains.model.SimilarDomain;
|
||||
@ -21,8 +24,6 @@ import nu.marginalia.search.model.UrlDetails;
|
||||
import nu.marginalia.search.svc.SearchFlagSiteService.FlagSiteFormData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
@ -68,17 +69,12 @@ public class SearchSiteInfoService {
|
||||
this.jteRenderer = jteRenderer;
|
||||
}
|
||||
|
||||
public Object handleOverview(Request request, Response response) {
|
||||
String domainName = request.queryParams("domain");
|
||||
if (domainName != null) {
|
||||
@GET
|
||||
@Path("/site")
|
||||
public ModelAndView<?> handleOverview(@PathParam String domain) {
|
||||
if (domain != null) {
|
||||
// redirect to /site/domainName
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; url=/site/%s">
|
||||
""".formatted(domainName);
|
||||
return new MapModelAndView("/redirect.jte", Map.of("url", "/site/"+domain));
|
||||
}
|
||||
|
||||
List<SiteOverviewModel.DiscoveredDomain> domains = new ArrayList<>();
|
||||
@ -95,7 +91,7 @@ public class SearchSiteInfoService {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return jteRenderer.render("siteinfo/start.jte",
|
||||
return new MapModelAndView("siteinfo/start.jte",
|
||||
Map.of("navbar", NavbarModel.SITEINFO,
|
||||
"model", new SiteOverviewModel(domains)));
|
||||
}
|
||||
@ -104,15 +100,20 @@ public class SearchSiteInfoService {
|
||||
public record DiscoveredDomain(String name, String timestamp) {}
|
||||
}
|
||||
|
||||
public Object handle(Request request, Response response) throws SQLException {
|
||||
String domainName = request.params("site");
|
||||
String view = request.queryParamOrDefault("view", "info");
|
||||
@GET
|
||||
@Path("/site/{domainName}")
|
||||
public ModelAndView<?> handle(
|
||||
@PathParam String domainName,
|
||||
@QueryParam String view,
|
||||
@QueryParam Integer page
|
||||
) throws SQLException {
|
||||
|
||||
if (null == domainName || domainName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int page = Integer.parseInt(request.queryParamOrDefault("page", "1"));
|
||||
page = Objects.requireNonNullElse(page, 1);
|
||||
view = Objects.requireNonNullElse(view, "info");
|
||||
|
||||
SiteInfoModel model = switch (view) {
|
||||
case "links" -> listLinks(domainName, page);
|
||||
@ -122,13 +123,20 @@ public class SearchSiteInfoService {
|
||||
default -> listInfo(domainName);
|
||||
};
|
||||
|
||||
return jteRenderer.render("siteinfo/main.jte",
|
||||
return new MapModelAndView("siteinfo/main.jte",
|
||||
Map.of("model", model, "navbar", NavbarModel.SITEINFO));
|
||||
}
|
||||
|
||||
public Object handlePost(Request request, Response response) throws SQLException {
|
||||
String domainName = request.params("site");
|
||||
String view = request.queryParamOrDefault("view", "info");
|
||||
@POST
|
||||
@Path("/site/{domainName}")
|
||||
public ModelAndView<?> handleComplaint(
|
||||
@PathParam String domainName,
|
||||
@QueryParam String view,
|
||||
@FormParam String category,
|
||||
@FormParam String description,
|
||||
@FormParam String samplequery
|
||||
|
||||
) throws SQLException {
|
||||
|
||||
if (null == domainName || domainName.isBlank()) {
|
||||
return null;
|
||||
@ -141,9 +149,9 @@ public class SearchSiteInfoService {
|
||||
|
||||
FlagSiteFormData formData = new FlagSiteFormData(
|
||||
domainId,
|
||||
request.queryParams("category"),
|
||||
request.queryParams("description"),
|
||||
request.queryParams("sampleQuery")
|
||||
category,
|
||||
description,
|
||||
samplequery
|
||||
);
|
||||
flagSiteService.insertComplaint(formData);
|
||||
|
||||
@ -151,7 +159,7 @@ public class SearchSiteInfoService {
|
||||
|
||||
var model = new ReportDomain(domainName, domainId, complaints, List.of(), true);
|
||||
|
||||
return jteRenderer.render("siteinfo/main.jte",
|
||||
return new MapModelAndView("siteinfo/main.jte",
|
||||
Map.of("model", model, "navbar", NavbarModel.SITEINFO));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
@param String url
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; ${url}">
|
||||
|
||||
<h1>Redirecting...</h1>
|
||||
|
||||
If this does not work, please try this link
|
||||
<p></p>
|
||||
<a href="${url}">${url}</a>.
|
||||
</html>
|
@ -5,6 +5,7 @@
|
||||
|
||||
@param NavbarModel navbar
|
||||
@param WebsiteUrl websiteUrl
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
@import nu.marginalia.search.svc.SearchFlagSiteService
|
||||
@import nu.marginalia.search.svc.SearchSiteInfoService.*
|
||||
|
||||
@param ReportDomain reportDomain
|
||||
@ -14,7 +15,6 @@
|
||||
instead of using this form.
|
||||
</div>
|
||||
@else
|
||||
|
||||
<form class="space-y-6 p-4" method="post">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">
|
||||
@ -22,10 +22,9 @@
|
||||
</label>
|
||||
<select required name="category" class="w-full px-3 py-2 bg-white border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">Select issue type...</option>
|
||||
<option value="spam">Spam</option>
|
||||
<option value="dead-link">Dead Link</option>
|
||||
<option value="inappropriate">Inappropriate Content</option>
|
||||
<option value="other">Other</option>
|
||||
@for (SearchFlagSiteService.CategoryItem item : SearchFlagSiteService.categories)
|
||||
<option value="${item.categoryName()}">${item.categoryDesc()}</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -570,6 +570,10 @@ video {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
@ -586,12 +590,8 @@ video {
|
||||
inset: 0px;
|
||||
}
|
||||
|
||||
.bottom-0 {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0px;
|
||||
.bottom-10 {
|
||||
bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.right-5 {
|
||||
@ -602,12 +602,12 @@ video {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.top-4 {
|
||||
top: 1rem;
|
||||
.top-2 {
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.top-5 {
|
||||
top: 1.25rem;
|
||||
.top-4 {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.z-50 {
|
||||
@ -725,6 +725,10 @@ video {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
@ -789,6 +793,10 @@ video {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
@ -829,6 +837,10 @@ video {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.basis-1\/2 {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -901,6 +913,12 @@ video {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.space-x-0 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0px * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
|
||||
@ -1001,6 +1019,10 @@ video {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
@ -1009,6 +1031,10 @@ video {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@ -1017,6 +1043,10 @@ video {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-none {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
.border-blue-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(191 219 254 / var(--tw-border-opacity, 1));
|
||||
@ -1091,10 +1121,6 @@ video {
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-gray-50\/90 {
|
||||
background-color: rgb(249 250 251 / 0.9);
|
||||
}
|
||||
|
||||
.bg-green-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
|
||||
@ -1478,6 +1504,12 @@ video {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
|
||||
@ -1490,6 +1522,14 @@ video {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.outline-1 {
|
||||
outline-width: 1px;
|
||||
}
|
||||
|
||||
.outline-margeblue {
|
||||
outline-color: #3e5f6f;
|
||||
}
|
||||
|
||||
.filter {
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
@ -1564,6 +1604,15 @@ video {
|
||||
--tw-ring-offset-width: 2px;
|
||||
}
|
||||
|
||||
.active\:text-slate-200:active {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.active\:outline:active {
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
.has-\[\:checked\]\:bg-gray-100:has(:checked) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
||||
@ -1590,10 +1639,6 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:static {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.sm\:m-0 {
|
||||
margin: 0px;
|
||||
}
|
||||
@ -1634,21 +1679,14 @@ video {
|
||||
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.sm\:border-none {
|
||||
border-style: none;
|
||||
.sm\:space-y-6 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.sm\:bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.sm\:p-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.sm\:p-3 {
|
||||
padding: 0.75rem;
|
||||
.sm\:p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sm\:px-2 {
|
||||
@ -1673,6 +1711,14 @@ video {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.md\:ml-16 {
|
||||
margin-left: 4rem;
|
||||
}
|
||||
|
||||
.md\:mr-16 {
|
||||
margin-right: 4rem;
|
||||
}
|
||||
|
||||
.md\:mr-8 {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
@ -1685,6 +1731,14 @@ video {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.md\:hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md\:w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.md\:w-64 {
|
||||
width: 16rem;
|
||||
}
|
||||
@ -1709,6 +1763,10 @@ video {
|
||||
place-items: start;
|
||||
}
|
||||
|
||||
.md\:place-items-baseline {
|
||||
place-items: baseline;
|
||||
}
|
||||
|
||||
.md\:gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
@ -1725,6 +1783,10 @@ video {
|
||||
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.md\:p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.md\:px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
File diff suppressed because it is too large
Load Diff
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- CC0 -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 455.731 455.731" xml:space="preserve">
|
||||
<g>
|
||||
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
|
||||
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
|
||||
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 891 B |
@ -1,831 +0,0 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
--clr-bg-page: hsl(60, 42%, 95%); // $nicotine-light
|
||||
|
||||
--clr-bg-ui: hsl(0, 0%, 100%);
|
||||
--clr-text-ui: #000; // $fg-dark
|
||||
|
||||
--clr-bg-theme: hsl(200, 28%, 34%); // $highlight-light
|
||||
--clr-text-theme: #fff; // $fg-light
|
||||
|
||||
--clr-bg-highlight: hsl(0, 0%, 93%); // $highlight-light2
|
||||
--clr-text-highlight: #111111;
|
||||
|
||||
--clr-bg-accent: hsl(63, 19%, 61%); // $nicotine-dark
|
||||
--clr-border-accent: hsl(63, 19%, 35%);
|
||||
|
||||
--clr-border: #aaa; // $border-color2
|
||||
|
||||
--clr-shadow: var(--clr-border);
|
||||
|
||||
--clr-link: #0066cc;
|
||||
--clr-link-visited: #531a89;
|
||||
--clr-heading-link-visited: #fcc; // $visited
|
||||
|
||||
--font-family: sans-serif;
|
||||
--font-size: 14px;
|
||||
--font-family-heading: serif; // $heading-fonts
|
||||
}
|
||||
|
||||
|
||||
@mixin dark-theme-mixin {
|
||||
color-scheme: dark;
|
||||
|
||||
--clr-bg-page: hsl(0, 0%, 6%);
|
||||
|
||||
--clr-bg-ui: hsl(0, 0%, 18%);
|
||||
--clr-text-ui: #ddd;
|
||||
|
||||
--clr-bg-theme: hsl(0, 0%, 2%);
|
||||
--clr-text-theme: var(--clr-text-ui);
|
||||
|
||||
--clr-bg-highlight: hsl(0, 0%, 11%);
|
||||
--clr-text-highlight: #fff;
|
||||
|
||||
--clr-bg-accent: hsl(200, 32%, 28%);
|
||||
--clr-border-accent: hsl(200, 8%, 12%);
|
||||
|
||||
--clr-border: hsl(0, 0%, 30%);
|
||||
|
||||
--clr-shadow: #000;
|
||||
|
||||
--clr-link: #8a8aff;
|
||||
--clr-link-visited: #ffadff;
|
||||
--clr-heading-link-visited: var(--clr-link-visited);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
@include dark-theme-mixin;
|
||||
}
|
||||
|
||||
// Makes theme match the user's OS preference when JS is disabled
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-has-js="true"]) {
|
||||
@include dark-theme-mixin;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--clr-link);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--clr-link-visited);
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1 a, h2 a {
|
||||
color: var(--clr-text-theme);
|
||||
}
|
||||
h1 a:visited, h2 a:visited {
|
||||
color: var(--clr-heading-link-visited);
|
||||
}
|
||||
progress {
|
||||
width: 10ch;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--clr-bg-page);
|
||||
color: var(--clr-text-ui);
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size);
|
||||
line-height: 1.6;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 120ch;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#frontpage {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-gap: 1ch;
|
||||
align-items: start;
|
||||
justify-content: start;
|
||||
margin-top: 1ch;
|
||||
margin-bottom: 1ch;
|
||||
// named grid areas
|
||||
grid-template-areas:
|
||||
"frontpage-about frontpage-news"
|
||||
"frontpage-tips frontpage-news";
|
||||
|
||||
@media (max-device-width: 624px) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-gap: 1ch;
|
||||
align-items: start;
|
||||
justify-content: start;
|
||||
margin-top: 1ch;
|
||||
margin-bottom: 1ch;
|
||||
// named grid areas
|
||||
grid-template-areas:
|
||||
"frontpage-about"
|
||||
"frontpage-tips"
|
||||
"frontpage-news";
|
||||
|
||||
* { max-width: unset !important; min-width: unset !important; }
|
||||
}
|
||||
|
||||
#frontpage-news {
|
||||
grid-area: frontpage-news;
|
||||
max-width: 40ch;
|
||||
@extend .dialog;
|
||||
}
|
||||
#frontpage-about {
|
||||
grid-area: frontpage-about;
|
||||
min-width: 40ch;
|
||||
@extend .dialog;
|
||||
}
|
||||
#frontpage-tips {
|
||||
grid-area: frontpage-tips;
|
||||
min-width: 40ch;
|
||||
@extend .dialog;
|
||||
}
|
||||
}
|
||||
|
||||
#siteinfo-nav {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@extend .dialog;
|
||||
padding: 0.25ch !important;
|
||||
margin-top: 1.5ch;
|
||||
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1ch;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
padding: 1ch;
|
||||
background-color: var(--clr-bg-highlight);
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
color: var(--clr-text-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
li.current {
|
||||
background-color: var(--clr-bg-theme);
|
||||
a {
|
||||
color: var(--clr-text-theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
border: 1px solid var(--clr-border);
|
||||
box-shadow: 0 0 1ch var(--clr-shadow);
|
||||
background-color: var(--clr-bg-ui);
|
||||
padding: 1ch;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
padding: 0.5ch;
|
||||
font-size: 12pt;
|
||||
background-color: var(--clr-bg-theme);
|
||||
color: var(--clr-text-theme);
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: var(--clr-bg-accent);
|
||||
border: 1px solid var(--clr-border-accent);
|
||||
color: var(--clr-text-ui);
|
||||
box-shadow: 0 0 0.5ch var(--clr-shadow);
|
||||
margin-bottom: 1ch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
nav {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--clr-text-ui);
|
||||
padding: .5ch;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--clr-text-ui);
|
||||
}
|
||||
|
||||
a.extra {
|
||||
background: #ccc linear-gradient(45deg,
|
||||
hsl(0, 100%, 70%) 0%,
|
||||
hsl(120, 100%, 70%) 50%,
|
||||
hsl(240, 100%, 70%) 100%);
|
||||
color: black;
|
||||
text-shadow: 0 0 0.5ch #fff;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
background: var(--clr-bg-theme);
|
||||
color: var(--clr-text-theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#theme {
|
||||
padding: .5ch;
|
||||
display: none;
|
||||
|
||||
[data-has-js='true'] & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#complaint {
|
||||
@extend .dialog;
|
||||
max-width: 60ch;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 2ch;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10ch;
|
||||
}
|
||||
}
|
||||
|
||||
#siteinfo {
|
||||
margin-top: 1ch;
|
||||
display: flex;
|
||||
gap: 1ch;
|
||||
flex-grow: 0.5;
|
||||
flex-shrink: 0.5;
|
||||
flex-basis: 10ch 10ch;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
|
||||
#index-info, #link-info {
|
||||
width: 32ch;
|
||||
@extend .dialog;
|
||||
}
|
||||
#screenshot {
|
||||
@extend .dialog;
|
||||
}
|
||||
#screenshot img {
|
||||
width: 30ch;
|
||||
height: 22.5ch;
|
||||
}
|
||||
}
|
||||
|
||||
.infobox {
|
||||
h2 {
|
||||
@extend .heading;
|
||||
}
|
||||
|
||||
background-color: var(--clr-bg-ui);
|
||||
padding: 1ch;
|
||||
margin: 1ch;
|
||||
border: 1px solid var(--clr-border);
|
||||
box-shadow: 0 0 1ch var(--clr-shadow);
|
||||
}
|
||||
|
||||
section.cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 1ch;
|
||||
gap: 2ch;
|
||||
justify-content: flex-start;
|
||||
|
||||
.card {
|
||||
background-color: var(--clr-bg-ui);
|
||||
border-left: 1px solid #ecb;
|
||||
border-top: 1px solid #ecb;
|
||||
box-shadow: var(--clr-shadow) 0 0 5px;
|
||||
|
||||
h2 {
|
||||
@extend .heading;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h2 a {
|
||||
display: block !important;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:focus img {
|
||||
filter: sepia(100%);
|
||||
box-shadow: #444 0px 0px 20px;
|
||||
}
|
||||
a:focus:not(.nofocus) {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
overflow: auto;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 28ch;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
[data-theme='dark'] & {
|
||||
border: 1px solid var(--clr-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.positions {
|
||||
box-shadow: 0 0 2px var(--clr-shadow);
|
||||
backdrop-filter: brightness(90%);
|
||||
color: var(--clr-text-highlight);
|
||||
padding: 2px;
|
||||
margin-right: -1ch;
|
||||
margin-left: 1ch;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
clear: both;
|
||||
|
||||
padding: 2ch;
|
||||
margin: 16ch 0 0 0;
|
||||
|
||||
font-size: 12pt;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
border-bottom: 4px solid var(--clr-bg-theme);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
font-weight: normal;
|
||||
border-bottom: 2px solid var(--clr-bg-theme);
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
section {
|
||||
line-height: 1.5;
|
||||
flex-basis: 40ch;
|
||||
flex-grow: 1.1;
|
||||
|
||||
background-color: var(--clr-bg-ui);
|
||||
border-left: 1px solid var(--clr-border);
|
||||
box-shadow: -1px -1px 5px var(--clr-shadow);
|
||||
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
margin-left: 1ch;
|
||||
padding-bottom: 1ch;
|
||||
margin-bottom: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
#mcfeast, #menu-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shadowbox {
|
||||
box-shadow: 0 0 1ch var(--clr-shadow);
|
||||
border: 1px solid var(--clr-border);
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
padding: 0.5ch;
|
||||
background-color: var(--clr-bg-theme);
|
||||
border-bottom: 1px solid var(--clr-border);
|
||||
font-family: var(--font-family-heading);
|
||||
font-weight: normal;
|
||||
color: var(--clr-text-theme);
|
||||
font-size: 12pt;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.sidebar-narrow {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
grid-gap: 1ch;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
#crosstalk-view {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-gap: 1ch;
|
||||
align-content: start;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
#similar-view {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-gap: 1ch;
|
||||
align-content: start;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
table {
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.screenshot {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#similar-view[data-layout="lopsided"] {
|
||||
#similar-info {
|
||||
@extend .dialog;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
#similar-domains {
|
||||
@extend .dialog;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
#similar-links {
|
||||
@extend .dialog;
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#similar-view[data-layout="balanced"] {
|
||||
#similar-info {
|
||||
@extend .dialog;
|
||||
}
|
||||
#similar-domains {
|
||||
grid-row: span 2;
|
||||
@extend .dialog;
|
||||
}
|
||||
#similar-links {
|
||||
@extend .dialog;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-device-width: 900px) {
|
||||
#similar-view, #crosstalk-view {
|
||||
display: block;
|
||||
* {
|
||||
margin-bottom: 1ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-device-width: 840px) {
|
||||
section.cards {
|
||||
display: block;
|
||||
.card {
|
||||
margin-bottom: 2ch;
|
||||
img {
|
||||
width: 100% !important;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#search-box {
|
||||
@extend .shadowbox;
|
||||
|
||||
padding: 0.5ch;
|
||||
background-color: var(--clr-bg-ui);
|
||||
display: grid;
|
||||
grid-template-columns: max-content 0 auto max-content;
|
||||
grid-gap: 0.5ch;
|
||||
grid-auto-rows: minmax(1ch, auto);
|
||||
width: 100%;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0.5ch;
|
||||
font-size: 14pt;
|
||||
word-break: keep-all;
|
||||
background-color: var(--clr-bg-theme);
|
||||
color: var(--clr-text-theme);
|
||||
font-family: var(--font-family-heading);
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#suggestions-anchor {
|
||||
margin: -0.5ch; // We need this anchor for the typeahead suggestions, but we don't want it to affect the layout
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
font-family: monospace;
|
||||
font-size: 12pt;
|
||||
padding: 0.5ch;
|
||||
border: 1px solid var(--clr-border);
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
font-size: 12pt;
|
||||
border: 1px solid var(--clr-border);
|
||||
background-color: var(--clr-bg-ui);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// white suggesitons looks fine in dark mode
|
||||
.suggestions {
|
||||
background-color: #fff;
|
||||
padding: .5ch;
|
||||
margin-top: 5.5ch;
|
||||
margin-left: 1ch;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
border-left: 1px solid #ccc;
|
||||
border-top: 1px solid #ccc;
|
||||
box-shadow: 5px 5px 5px var(--clr-shadow);
|
||||
z-index: 10;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: #000;
|
||||
font-size: 12pt;
|
||||
font-family: 'fixedsys', monospace, serif;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
display: block;
|
||||
background-color: #000;
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.filter-toggle-on {
|
||||
a:before {
|
||||
content: '✓';
|
||||
margin-right: 1.5ch;
|
||||
}
|
||||
}
|
||||
.filter-toggle-off {
|
||||
a:before {
|
||||
content: '✗';
|
||||
margin-right: 1.5ch;
|
||||
}
|
||||
}
|
||||
|
||||
#filters {
|
||||
@extend .shadowbox;
|
||||
margin-top: 1ch;
|
||||
background-color: var(--clr-bg-ui);
|
||||
|
||||
h2 {
|
||||
@extend .heading;
|
||||
background-color: var(--clr-bg-theme);
|
||||
}
|
||||
h3 {
|
||||
@extend .heading;
|
||||
background-color: var(--clr-bg-highlight);
|
||||
color: var(--clr-text-highlight);
|
||||
font-family: sans-serif;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 0.5px solid var(--clr-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
padding: 1ch;
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover, a:focus {
|
||||
border-bottom: 1px solid var(--clr-bg-theme);
|
||||
}
|
||||
}
|
||||
|
||||
li.current {
|
||||
border-left: 4px solid var(--clr-bg-theme);
|
||||
background-color: var(--clr-bg-highlight);
|
||||
a {
|
||||
margin-left: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result {
|
||||
@extend .shadowbox;
|
||||
margin: 1ch 0 2ch 0;
|
||||
|
||||
.url {
|
||||
background-color: var(--clr-bg-theme);
|
||||
padding-left: 0.5ch;
|
||||
|
||||
a {
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
font-size: 8pt;
|
||||
color: var(--clr-text-theme);
|
||||
text-shadow: 0 0 1ch #000; // guarantee decent contrast across background colors
|
||||
}
|
||||
a:visited {
|
||||
color: var(--clr-heading-link-visited);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
a {
|
||||
word-break: break-all;
|
||||
color: var(--clr-text-ui);
|
||||
text-decoration: none;
|
||||
}
|
||||
font-size: 12pt;
|
||||
@extend .heading;
|
||||
background-color:var(--clr-bg-highlight);
|
||||
}
|
||||
|
||||
.description {
|
||||
background-color: var(--clr-bg-ui);
|
||||
word-break: break-word;
|
||||
padding: 1ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.additional-results {
|
||||
background-color: var(--clr-bg-ui);
|
||||
padding: 1ch;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result[data-ms-rank="1"] { .url, h2 { filter: grayscale(0%); } }
|
||||
.search-result[data-ms-rank="2"] { .url, h2 { filter: grayscale(5%); } }
|
||||
.search-result[data-ms-rank="3"] { .url, h2 { filter: grayscale(15%); } }
|
||||
.search-result[data-ms-rank="4"] { .url, h2 { filter: grayscale(20%); } }
|
||||
.search-result[data-ms-rank="5"] { .url, h2 { filter: grayscale(30%); } }
|
||||
.search-result[data-ms-rank="10"] { .url, h2 { filter: grayscale(60%); } }
|
||||
|
||||
.utils {
|
||||
display: flex;
|
||||
font-size: 10pt;
|
||||
padding: 1ch;
|
||||
background-color: var(--clr-bg-highlight);
|
||||
|
||||
> * {
|
||||
margin-right: 1ch;
|
||||
margin-left: 1ch;
|
||||
}
|
||||
.meta {
|
||||
flex-grow: 2;
|
||||
text-align: right;
|
||||
}
|
||||
.meta > * {
|
||||
padding-left: 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--clr-text-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-device-width: 624px) {
|
||||
[data-has-js="true"] body { // This property is set via js so we can selectively enable these changes only if JS is enabled;
|
||||
// This is desirable since mobile navigation is JS-driven. If JS is disabled, having a squished
|
||||
// GUI is better than having no working UI.
|
||||
margin: 0 !important;
|
||||
padding: 0 0 0 0 !important;
|
||||
max-width: 100%;
|
||||
|
||||
#suggestions-anchor { display: none; } // suggestions are not useful on mobile
|
||||
|
||||
.sidebar-narrow {
|
||||
display: block; // fix for bizarre chrome rendering issue
|
||||
}
|
||||
|
||||
#mcfeast {
|
||||
display: inline;
|
||||
float: right;
|
||||
width: 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#menu-close {
|
||||
float: right;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#filters {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-narrow {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
#search-box {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
#filters {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-link {
|
||||
padding-top: 0.25ch;
|
||||
padding-bottom: 0.25ch;
|
||||
padding-left: 0.5ch;
|
||||
padding-right: 0.5ch;
|
||||
margin-right: 0.5ch;
|
||||
|
||||
font-size: 12pt;
|
||||
border: 1px solid var(--clr-border);
|
||||
background-color: var(--clr-bg-highlight);
|
||||
color: var(--clr-text-ui) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-link.active {
|
||||
border: 1px solid var(--clr-text-ui);
|
||||
background-color: var(--clr-bg-ui);
|
||||
}
|
||||
|
||||
// The search results page is very confusing on text-based browsers, so we add a hr to separate the search results. This is
|
||||
// hidden on modern browsers via CSS.
|
||||
|
||||
hr.w3m-helper { display: none; }
|
||||
|
||||
// This is a screenreader-only class that hides content from visual browsers, but allows screenreaders and
|
||||
// text-based browsers to access it.
|
||||
|
||||
.screenreader-only {
|
||||
position:absolute;
|
||||
left:-10000px;
|
||||
top:auto;
|
||||
width:1px;
|
||||
height:1px;
|
||||
overflow:hidden;
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.exceptions.RedirectException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BangCommandTest {
|
||||
public BangCommand bangCommand = new BangCommand();
|
||||
|
||||
@Test
|
||||
public void testG() {
|
||||
try {
|
||||
bangCommand.process(null,
|
||||
new SearchParameters(null, " !g test",
|
||||
null, null, null, null, null, false, 1)
|
||||
);
|
||||
Assertions.fail("Should have thrown RedirectException");
|
||||
}
|
||||
catch (RedirectException ex) {
|
||||
assertEquals("https://www.google.com/search?q=test", ex.newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchPattern() {
|
||||
var match = bangCommand.matchBangPattern("!g test", "!g");
|
||||
|
||||
assertTrue(match.isPresent());
|
||||
assertEquals(match.get(), "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchPattern2() {
|
||||
var match = bangCommand.matchBangPattern("test !g", "!g");
|
||||
|
||||
assertTrue(match.isPresent());
|
||||
assertEquals(match.get(), "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchPattern3() {
|
||||
var match = bangCommand.matchBangPattern("hello !g world", "!g");
|
||||
|
||||
assertTrue(match.isPresent());
|
||||
assertEquals(match.get(), "hello world");
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ package nu.marginalia.status;
|
||||
import com.google.inject.Inject;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import nu.marginalia.status.db.StatusMetricDb;
|
||||
import nu.marginalia.status.endpoints.ApiEndpoint;
|
||||
import nu.marginalia.status.endpoints.MainSearchEndpoint;
|
||||
@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static spark.Spark.get;
|
||||
|
||||
public class StatusService extends Service {
|
||||
public class StatusService extends SparkService {
|
||||
private final ScheduledExecutorService scheduledExecutorService =
|
||||
Executors.newScheduledThreadPool(5);
|
||||
|
||||
|
@ -11,7 +11,7 @@ import nu.marginalia.rss.svc.FeedsGrpcService;
|
||||
import nu.marginalia.screenshot.ScreenshotService;
|
||||
import nu.marginalia.service.discovery.property.ServicePartition;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
@ -20,7 +20,7 @@ import spark.Spark;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AssistantService extends Service {
|
||||
public class AssistantService extends SparkService {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private final Gson gson = GsonFactory.get();
|
||||
private final Suggestions suggestions;
|
||||
|
@ -12,7 +12,7 @@ import nu.marginalia.model.gson.GsonFactory;
|
||||
import nu.marginalia.screenshot.ScreenshotService;
|
||||
import nu.marginalia.service.ServiceMonitors;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import nu.marginalia.service.server.StaticResources;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -22,7 +22,7 @@ import spark.Spark;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ControlService extends Service {
|
||||
public class ControlService extends SparkService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private final Gson gson = GsonFactory.get();
|
||||
|
@ -4,7 +4,7 @@ import com.google.inject.Inject;
|
||||
import nu.marginalia.execution.*;
|
||||
import nu.marginalia.service.discovery.property.ServicePartition;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import nu.marginalia.service.server.mq.MqRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -13,7 +13,7 @@ import spark.Spark;
|
||||
import java.util.List;
|
||||
|
||||
// Weird name for this one to not have clashes with java.util.concurrent.ExecutorService
|
||||
public class ExecutorSvc extends Service {
|
||||
public class ExecutorSvc extends SparkService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExecutorSvc.class);
|
||||
private final ExecutionInit executionInit;
|
||||
|
@ -11,7 +11,7 @@ import nu.marginalia.service.control.ServiceEventLog;
|
||||
import nu.marginalia.service.discovery.property.ServicePartition;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Initialization;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import nu.marginalia.service.server.mq.MqRequest;
|
||||
import nu.marginalia.storage.FileStorageService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -25,7 +25,7 @@ import java.util.List;
|
||||
import static nu.marginalia.linkdb.LinkdbFileNames.DOCDB_FILE_NAME;
|
||||
import static nu.marginalia.linkdb.LinkdbFileNames.DOMAIN_LINKS_FILE_NAME;
|
||||
|
||||
public class IndexService extends Service {
|
||||
public class IndexService extends SparkService {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@NotNull
|
||||
|
@ -5,7 +5,7 @@ import nu.marginalia.functions.searchquery.QueryGRPCService;
|
||||
import nu.marginalia.linkgraph.AggregateLinkGraphService;
|
||||
import nu.marginalia.service.discovery.property.ServicePartition;
|
||||
import nu.marginalia.service.server.BaseServiceParams;
|
||||
import nu.marginalia.service.server.Service;
|
||||
import nu.marginalia.service.server.SparkService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Spark;
|
||||
@ -13,7 +13,7 @@ import spark.Spark;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class QueryService extends Service {
|
||||
public class QueryService extends SparkService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(QueryService.class);
|
||||
|
||||
|
@ -101,6 +101,10 @@ include 'third-party:commons-codec'
|
||||
include 'third-party:parquet-floor'
|
||||
include 'third-party:encyclopedia-marginalia-nu'
|
||||
|
||||
ext {
|
||||
joobyVersion = '3.5.5'
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
|
||||
repositories {
|
||||
@ -230,6 +234,10 @@ dependencyResolutionManagement {
|
||||
library('jetty-util','org.eclipse.jetty','jetty-util').version('9.4.54.v20240208')
|
||||
library('jetty-servlet','org.eclipse.jetty','jetty-servlet').version('9.4.54.v20240208')
|
||||
|
||||
library('jooby-netty','io.jooby','jooby-netty').version(joobyVersion)
|
||||
library('jooby-jte','io.jooby','jooby-jte').version(joobyVersion)
|
||||
library('jooby-apt','io.jooby','jooby-apt').version(joobyVersion)
|
||||
|
||||
library('jte','gg.jte','jte').version('3.1.15')
|
||||
|
||||
library('slop', 'nu.marginalia', 'slop').version('0.0.8-SNAPSHOT')
|
||||
@ -250,7 +258,7 @@ dependencyResolutionManagement {
|
||||
bundle('parquet', ['parquet-column', 'parquet-hadoop'])
|
||||
bundle('junit', ['junit.jupiter', 'junit.jupiter.engine'])
|
||||
bundle('flyway', ['flyway.core', 'flyway.mysql'])
|
||||
|
||||
bundle('jooby', ['jooby-netty', 'jooby-jte'])
|
||||
bundle('curator', ['curator-framework', 'curator-x-discovery'])
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user