diff --git a/build.gradle b/build.gradle index a5cd492e..ce547a8d 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ ext { dockerImageTag='latest' dockerImageRegistry='marginalia' jibVersion = '3.4.3' + } idea { diff --git a/code/common/service/build.gradle b/code/common/service/build.gradle index 4b2b4f1d..33b716bd 100644 --- a/code/common/service/build.gradle +++ b/code/common/service/build.gradle @@ -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 diff --git a/code/common/service/java/nu/marginalia/service/server/JoobyService.java b/code/common/service/java/nu/marginalia/service/server/JoobyService.java new file mode 100644 index 00000000..fafdafb5 --- /dev/null +++ b/code/common/service/java/nu/marginalia/service/server/JoobyService.java @@ -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 joobyServices; + private final ServiceEndpoint restEndpoint; + + public JoobyService(BaseServiceParams params, + ServicePartition partition, + List grpcServices, + List 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(); + } + } + +} \ No newline at end of file diff --git a/code/common/service/java/nu/marginalia/service/server/Service.java b/code/common/service/java/nu/marginalia/service/server/SparkService.java similarity index 91% rename from code/common/service/java/nu/marginalia/service/server/Service.java rename to code/common/service/java/nu/marginalia/service/server/SparkService.java index 47cf5ce7..8fc0a726 100644 --- a/code/common/service/java/nu/marginalia/service/server/Service.java +++ b/code/common/service/java/nu/marginalia/service/server/SparkService.java @@ -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 grpcServices) throws Exception { + public SparkService(BaseServiceParams params, + Runnable configureStaticFiles, + ServicePartition partition, + List 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 grpcServices) throws Exception { + public SparkService(BaseServiceParams params, + ServicePartition partition, + List 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()); } diff --git a/code/common/service/java/nu/marginalia/service/server/jte/JteModule.java b/code/common/service/java/nu/marginalia/service/server/jte/JteModule.java new file mode 100644 index 00000000..b2bf1d29 --- /dev/null +++ b/code/common/service/java/nu/marginalia/service/server/jte/JteModule.java @@ -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> 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); + } + } +} diff --git a/code/common/service/java/nu/marginalia/service/server/jte/JteTemplateEngine.java b/code/common/service/java/nu/marginalia/service/server/jte/JteTemplateEngine.java new file mode 100644 index 00000000..e1777ae4 --- /dev/null +++ b/code/common/service/java/nu/marginalia/service/server/jte/JteTemplateEngine.java @@ -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 extensions; + + public JteTemplateEngine(TemplateEngine jte) { + this.jte = jte; + this.extensions = List.of(".jte", ".kte"); + } + + + @NonNull @Override + public List 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(); + mapModel.putAll(attributes); + mapModel.putAll(mapModelAndView.getModel()); + jte.render(modelAndView.getView(), mapModel, output); + } else { + jte.render(modelAndView.getView(), modelAndView.getModel(), output); + } + + return buffer; + } +} \ No newline at end of file diff --git a/code/common/service/java/nu/marginalia/service/server/mq/ServiceMqSubscription.java b/code/common/service/java/nu/marginalia/service/server/mq/ServiceMqSubscription.java index 868a545e..2fe111de 100644 --- a/code/common/service/java/nu/marginalia/service/server/mq/ServiceMqSubscription.java +++ b/code/common/service/java/nu/marginalia/service/server/mq/ServiceMqSubscription.java @@ -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 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 diff --git a/code/services-application/api-service/java/nu/marginalia/api/ApiService.java b/code/services-application/api-service/java/nu/marginalia/api/ApiService.java index f9ac64f6..e0949853 100644 --- a/code/services-application/api-service/java/nu/marginalia/api/ApiService.java +++ b/code/services-application/api-service/java/nu/marginalia/api/ApiService.java @@ -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(); diff --git a/code/services-application/dating-service/java/nu/marginalia/dating/DatingService.java b/code/services-application/dating-service/java/nu/marginalia/dating/DatingService.java index b3325408..78c78189 100644 --- a/code/services-application/dating-service/java/nu/marginalia/dating/DatingService.java +++ b/code/services-application/dating-service/java/nu/marginalia/dating/DatingService.java @@ -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; diff --git a/code/services-application/explorer-service/java/nu/marginalia/explorer/ExplorerService.java b/code/services-application/explorer-service/java/nu/marginalia/explorer/ExplorerService.java index d4e69ecc..47a71eba 100644 --- a/code/services-application/explorer-service/java/nu/marginalia/explorer/ExplorerService.java +++ b/code/services-application/explorer-service/java/nu/marginalia/explorer/ExplorerService.java @@ -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 renderer; private final HikariDataSource dataSource; diff --git a/code/services-application/search-service/build.gradle b/code/services-application/search-service/build.gradle index 126355ea..3e50bcf8 100644 --- a/code/services-application/search-service/build.gradle +++ b/code/services-application/search-service/build.gradle @@ -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 diff --git a/code/services-application/search-service/java/nu/marginalia/search/SearchMain.java b/code/services-application/search-service/java/nu/marginalia/search/SearchMain.java index 37b9893d..1f4a60dd 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/SearchMain.java +++ b/code/services-application/search-service/java/nu/marginalia/search/SearchMain.java @@ -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); } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/SearchService.java b/code/services-application/search-service/java/nu/marginalia/search/SearchService.java index 1ba1e8bf..31abe74d 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/SearchService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/SearchService.java @@ -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 ""; +// } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/CommandEvaluator.java b/code/services-application/search-service/java/nu/marginalia/search/command/CommandEvaluator.java index 871fadfe..e9b684d4 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/CommandEvaluator.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/CommandEvaluator.java @@ -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(); } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/SearchCommandInterface.java b/code/services-application/search-service/java/nu/marginalia/search/command/SearchCommandInterface.java index d69bacbd..cc4f3b18 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/SearchCommandInterface.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/SearchCommandInterface.java @@ -1,10 +1,9 @@ package nu.marginalia.search.command; - -import spark.Response; +import io.jooby.ModelAndView; import java.util.Optional; public interface SearchCommandInterface { - Optional process(Response response, SearchParameters parameters); + Optional> process(SearchParameters parameters) throws Exception; } diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/SearchParameters.java b/code/services-application/search-service/java/nu/marginalia/search/command/SearchParameters.java index 39d64b1e..5c8654c7 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/SearchParameters.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/SearchParameters.java @@ -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; diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/commands/BangCommand.java b/code/services-application/search-service/java/nu/marginalia/search/command/commands/BangCommand.java index 049456e7..09dd9092 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/commands/BangCommand.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/commands/BangCommand.java @@ -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 process(Response response, SearchParameters parameters) { + public Optional> 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)); } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/commands/BrowseRedirectCommand.java b/code/services-application/search-service/java/nu/marginalia/search/command/commands/BrowseRedirectCommand.java index 1b57ebca..566d38cc 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/commands/BrowseRedirectCommand.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/commands/BrowseRedirectCommand.java @@ -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 process(Response response, SearchParameters parameters) { + public Optional> 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(""" - - - - Redirecting... - - """.formatted(redirectPath)); + return Optional.of( + new MapModelAndView("/redirect.jte", Map.of("url", redirectPath)) + ); } diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/commands/ConvertCommand.java b/code/services-application/search-service/java/nu/marginalia/search/command/commands/ConvertCommand.java index f4888556..85e8b0ef 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/commands/ConvertCommand.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/commands/ConvertCommand.java @@ -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 process(Response response, SearchParameters parameters) { + public Optional> 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) diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/commands/DefinitionCommand.java b/code/services-application/search-service/java/nu/marginalia/search/command/commands/DefinitionCommand.java index 77a5d828..2606b1fc 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/commands/DefinitionCommand.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/commands/DefinitionCommand.java @@ -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 process(Response response, SearchParameters parameters) { + public Optional> 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) diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/commands/SearchCommand.java b/code/services-application/search-service/java/nu/marginalia/search/command/commands/SearchCommand.java index 9ce1e9d0..419bf394 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/commands/SearchCommand.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/commands/SearchCommand.java @@ -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 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> 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) + )); } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/command/commands/SiteRedirectCommand.java b/code/services-application/search-service/java/nu/marginalia/search/command/commands/SiteRedirectCommand.java index 902ed025..3d2d5f4a 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/command/commands/SiteRedirectCommand.java +++ b/code/services-application/search-service/java/nu/marginalia/search/command/commands/SiteRedirectCommand.java @@ -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 process(Response response, SearchParameters parameters) { + public Optional> 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(""" - - - - Redirecting... - - """.formatted(domain, view) - ); + String url = "/site/%s?view=%s".formatted(domain, view); + return Optional.of(new MapModelAndView("/redirect.jte", Map.of("url", url))); } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/exceptions/RedirectException.java b/code/services-application/search-service/java/nu/marginalia/search/exceptions/RedirectException.java deleted file mode 100644 index eb04a4cb..00000000 --- a/code/services-application/search-service/java/nu/marginalia/search/exceptions/RedirectException.java +++ /dev/null @@ -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]; - } -} diff --git a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchAddToCrawlQueueService.java b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchAddToCrawlQueueService.java index a5f080bf..0471b6f1 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchAddToCrawlQueueService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchAddToCrawlQueueService.java @@ -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(); } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchBrowseService.java b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchBrowseService.java index ceab6b7b..e9da627a 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchBrowseService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchBrowseService.java @@ -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 ) diff --git a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchCrosstalkService.java b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchCrosstalkService.java index c6881e96..ca7367b1 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchCrosstalkService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchCrosstalkService.java @@ -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)); diff --git a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchErrorPageService.java b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchErrorPageService.java index 9ca9a391..321b2a9d 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchErrorPageService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchErrorPageService.java @@ -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) ) ) - )); + ); } } diff --git a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchFrontPageService.java b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchFrontPageService.java index 61bb1e39..5c2ee800 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchFrontPageService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchFrontPageService.java @@ -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 newsItems = getNewsItems(); @@ -112,7 +106,7 @@ public class SearchFrontPageService { response.type("application/rss+xml"); return sb.toString(); - } + }*/ private record IndexModel(List news, int searchPerMinute) { } private record NewsItem(String title, String url, String source, LocalDate date) {} diff --git a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchQueryService.java b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchQueryService.java index 5fd6a5b9..965a6637 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchQueryService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchQueryService.java @@ -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)); } } + } diff --git a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchSiteInfoService.java b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchSiteInfoService.java index 4d2ea831..c296f33c 100644 --- a/code/services-application/search-service/java/nu/marginalia/search/svc/SearchSiteInfoService.java +++ b/code/services-application/search-service/java/nu/marginalia/search/svc/SearchSiteInfoService.java @@ -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 """ - - - - Redirecting... - - """.formatted(domainName); + return new MapModelAndView("/redirect.jte", Map.of("url", "/site/"+domain)); } List 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)); } diff --git a/code/services-application/search-service/resources/jte/redirect.jte b/code/services-application/search-service/resources/jte/redirect.jte new file mode 100644 index 00000000..c583dc25 --- /dev/null +++ b/code/services-application/search-service/resources/jte/redirect.jte @@ -0,0 +1,14 @@ +@param String url + + + + +Redirecting... + + +

Redirecting...

+ +If this does not work, please try this link +

+${url}. + \ No newline at end of file diff --git a/code/services-application/search-service/resources/jte/serp/first.jte b/code/services-application/search-service/resources/jte/serp/first.jte index afca013f..74451c70 100644 --- a/code/services-application/search-service/resources/jte/serp/first.jte +++ b/code/services-application/search-service/resources/jte/serp/first.jte @@ -5,6 +5,7 @@ @param NavbarModel navbar @param WebsiteUrl websiteUrl + diff --git a/code/services-application/search-service/resources/jte/siteinfo/view/reportDomain.jte b/code/services-application/search-service/resources/jte/siteinfo/view/reportDomain.jte index 88fb4ac3..cfa5fcea 100644 --- a/code/services-application/search-service/resources/jte/siteinfo/view/reportDomain.jte +++ b/code/services-application/search-service/resources/jte/siteinfo/view/reportDomain.jte @@ -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. @else -
diff --git a/code/services-application/search-service/resources/static/search/crawler-ips.txt b/code/services-application/search-service/resources/static/crawler-ips.txt similarity index 100% rename from code/services-application/search-service/resources/static/search/crawler-ips.txt rename to code/services-application/search-service/resources/static/crawler-ips.txt diff --git a/code/services-application/search-service/resources/static/search/css/fa-all.min.css b/code/services-application/search-service/resources/static/css/fa-all.min.css similarity index 100% rename from code/services-application/search-service/resources/static/search/css/fa-all.min.css rename to code/services-application/search-service/resources/static/css/fa-all.min.css diff --git a/code/services-application/search-service/resources/static/css/style.css b/code/services-application/search-service/resources/static/css/style.css index e269fd07..3ff38772 100644 --- a/code/services-application/search-service/resources/static/css/style.css +++ b/code/services-application/search-service/resources/static/css/style.css @@ -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; diff --git a/code/services-application/search-service/resources/static/search/favicon.ico b/code/services-application/search-service/resources/static/favicon.ico similarity index 100% rename from code/services-application/search-service/resources/static/search/favicon.ico rename to code/services-application/search-service/resources/static/favicon.ico diff --git a/code/services-application/search-service/resources/static/search/js/mobile-button.js b/code/services-application/search-service/resources/static/js/mobile-button.js similarity index 100% rename from code/services-application/search-service/resources/static/search/js/mobile-button.js rename to code/services-application/search-service/resources/static/js/mobile-button.js diff --git a/code/services-application/search-service/resources/static/search/js/typeahead.js b/code/services-application/search-service/resources/static/js/typeahead.js similarity index 100% rename from code/services-application/search-service/resources/static/search/js/typeahead.js rename to code/services-application/search-service/resources/static/js/typeahead.js diff --git a/code/services-application/search-service/resources/static/search/opensearch.xml b/code/services-application/search-service/resources/static/opensearch.xml similarity index 100% rename from code/services-application/search-service/resources/static/search/opensearch.xml rename to code/services-application/search-service/resources/static/opensearch.xml diff --git a/code/services-application/search-service/resources/static/search/robots.txt b/code/services-application/search-service/resources/static/robots.txt similarity index 100% rename from code/services-application/search-service/resources/static/search/robots.txt rename to code/services-application/search-service/resources/static/robots.txt diff --git a/code/services-application/search-service/resources/static/search/css/style.css b/code/services-application/search-service/resources/static/search/css/style.css deleted file mode 100644 index 3ff38772..00000000 --- a/code/services-application/search-service/resources/static/search/css/style.css +++ /dev/null @@ -1,1814 +0,0 @@ -*, ::before, ::after { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -/* -! tailwindcss v3.4.16 | MIT License | https://tailwindcss.com -*/ - -/* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, -::before, -::after { - box-sizing: border-box; - /* 1 */ - border-width: 0; - /* 2 */ - border-style: solid; - /* 2 */ - border-color: #e5e7eb; - /* 2 */ -} - -::before, -::after { - --tw-content: ''; -} - -/* -1. Use a consistent sensible line-height in all browsers. -2. Prevent adjustments of font size after orientation changes in iOS. -3. Use a more readable tab size. -4. Use the user's configured `sans` font-family by default. -5. Use the user's configured `sans` font-feature-settings by default. -6. Use the user's configured `sans` font-variation-settings by default. -7. Disable tap highlights on iOS -*/ - -html, -:host { - line-height: 1.5; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -moz-tab-size: 4; - /* 3 */ - -o-tab-size: 4; - tab-size: 4; - /* 3 */ - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - /* 4 */ - font-feature-settings: normal; - /* 5 */ - font-variation-settings: normal; - /* 6 */ - -webkit-tap-highlight-color: transparent; - /* 7 */ -} - -/* -1. Remove the margin in all browsers. -2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. -*/ - -body { - margin: 0; - /* 1 */ - line-height: inherit; - /* 2 */ -} - -/* -1. Add the correct height in Firefox. -2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) -3. Ensure horizontal rules are visible by default. -*/ - -hr { - height: 0; - /* 1 */ - color: inherit; - /* 2 */ - border-top-width: 1px; - /* 3 */ -} - -/* -Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* -Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* -Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - text-decoration: inherit; -} - -/* -Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* -1. Use the user's configured `mono` font-family by default. -2. Use the user's configured `mono` font-feature-settings by default. -3. Use the user's configured `mono` font-variation-settings by default. -4. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - /* 1 */ - font-feature-settings: normal; - /* 2 */ - font-variation-settings: normal; - /* 3 */ - font-size: 1em; - /* 4 */ -} - -/* -Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* -Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* -1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) -2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) -3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; - /* 1 */ - border-color: inherit; - /* 2 */ - border-collapse: collapse; - /* 3 */ -} - -/* -1. Change the font styles in all browsers. -2. Remove the margin in Firefox and Safari. -3. Remove default padding in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - /* 1 */ - font-feature-settings: inherit; - /* 1 */ - font-variation-settings: inherit; - /* 1 */ - font-size: 100%; - /* 1 */ - font-weight: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - letter-spacing: inherit; - /* 1 */ - color: inherit; - /* 1 */ - margin: 0; - /* 2 */ - padding: 0; - /* 3 */ -} - -/* -Remove the inheritance of text transform in Edge and Firefox. -*/ - -button, -select { - text-transform: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Remove default button styles. -*/ - -button, -input:where([type='button']), -input:where([type='reset']), -input:where([type='submit']) { - -webkit-appearance: button; - /* 1 */ - background-color: transparent; - /* 2 */ - background-image: none; - /* 2 */ -} - -/* -Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* -Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* -Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* -Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* -1. Correct the odd appearance in Chrome and Safari. -2. Correct the outline style in Safari. -*/ - -[type='search'] { - -webkit-appearance: textfield; - /* 1 */ - outline-offset: -2px; - /* 2 */ -} - -/* -Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Change font properties to `inherit` in Safari. -*/ - -::-webkit-file-upload-button { - -webkit-appearance: button; - /* 1 */ - font: inherit; - /* 2 */ -} - -/* -Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* -Removes the default spacing and border for appropriate elements. -*/ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -fieldset { - margin: 0; - padding: 0; -} - -legend { - padding: 0; -} - -ol, -ul, -menu { - list-style: none; - margin: 0; - padding: 0; -} - -/* -Reset default styling for dialogs. -*/ - -dialog { - padding: 0; -} - -/* -Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* -1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -2. Set the default placeholder color to the user's configured gray 400 color. -*/ - -input::-moz-placeholder, textarea::-moz-placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -input::placeholder, -textarea::placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -/* -Set the default cursor for buttons. -*/ - -button, -[role="button"] { - cursor: pointer; -} - -/* -Make sure disabled buttons don't get the pointer cursor. -*/ - -:disabled { - cursor: default; -} - -/* -1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) -2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - /* 1 */ - vertical-align: middle; - /* 2 */ -} - -/* -Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* Make elements with the HTML hidden attribute stay hidden by default */ - -[hidden]:where(:not([hidden="until-found"])) { - display: none; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.visible { - visibility: visible; -} - -.static { - position: static; -} - -.fixed { - position: fixed; -} - -.absolute { - position: absolute; -} - -.sticky { - position: sticky; -} - -.inset-0 { - inset: 0px; -} - -.bottom-10 { - bottom: 2.5rem; -} - -.right-5 { - right: 1.25rem; -} - -.top-0 { - top: 0px; -} - -.top-2 { - top: 0.5rem; -} - -.top-4 { - top: 1rem; -} - -.z-50 { - z-index: 50; -} - -.m-4 { - margin: 1rem; -} - -.mx-1 { - margin-left: 0.25rem; - margin-right: 0.25rem; -} - -.mx-2 { - margin-left: 0.5rem; - margin-right: 0.5rem; -} - -.mx-3 { - margin-left: 0.75rem; - margin-right: 0.75rem; -} - -.mx-4 { - margin-left: 1rem; - margin-right: 1rem; -} - -.mx-5 { - margin-left: 1.25rem; - margin-right: 1.25rem; -} - -.mx-8 { - margin-left: 2rem; - margin-right: 2rem; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.my-10 { - margin-top: 2.5rem; - margin-bottom: 2.5rem; -} - -.my-3 { - margin-top: 0.75rem; - margin-bottom: 0.75rem; -} - -.my-4 { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.my-5 { - margin-top: 1.25rem; - margin-bottom: 1.25rem; -} - -.mb-1 { - margin-bottom: 0.25rem; -} - -.mb-2 { - margin-bottom: 0.5rem; -} - -.mb-3 { - margin-bottom: 0.75rem; -} - -.mb-4 { - margin-bottom: 1rem; -} - -.ml-2 { - margin-left: 0.5rem; -} - -.ml-4 { - margin-left: 1rem; -} - -.ml-5 { - margin-left: 1.25rem; -} - -.ml-6 { - margin-left: 1.5rem; -} - -.mr-2 { - margin-right: 0.5rem; -} - -.mr-3 { - margin-right: 0.75rem; -} - -.mr-4 { - margin-right: 1rem; -} - -.mr-8 { - margin-right: 2rem; -} - -.mt-1 { - margin-top: 0.25rem; -} - -.mt-10 { - margin-top: 2.5rem; -} - -.mt-2 { - margin-top: 0.5rem; -} - -.mt-4 { - margin-top: 1rem; -} - -.mt-8 { - margin-top: 2rem; -} - -.block { - display: block; -} - -.flex { - display: flex; -} - -.table { - display: table; -} - -.grid { - display: grid; -} - -.hidden { - display: none; -} - -.h-2 { - height: 0.5rem; -} - -.h-32 { - height: 8rem; -} - -.h-4 { - height: 1rem; -} - -.min-h-screen { - min-height: 100vh; -} - -.w-16 { - width: 4rem; -} - -.w-32 { - width: 8rem; -} - -.w-4 { - width: 1rem; -} - -.w-64 { - width: 16rem; -} - -.w-96 { - width: 24rem; -} - -.w-full { - width: 100%; -} - -.min-w-full { - min-width: 100%; -} - -.max-w-2xl { - max-width: 42rem; -} - -.max-w-3xl { - max-width: 48rem; -} - -.max-w-\[1000px\] { - max-width: 1000px; -} - -.max-w-\[1400px\] { - max-width: 1400px; -} - -.max-w-md { - max-width: 28rem; -} - -.flex-1 { - flex: 1 1 0%; -} - -.shrink-0 { - flex-shrink: 0; -} - -.grow { - flex-grow: 1; -} - -.basis-1\/2 { - flex-basis: 50%; -} - -.cursor-pointer { - cursor: pointer; -} - -.list-disc { - list-style-type: disc; -} - -.grid-cols-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); -} - -.grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.grid-cols-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -.flex-row { - flex-direction: row; -} - -.flex-col { - flex-direction: column; -} - -.place-items-center { - place-items: center; -} - -.place-items-baseline { - place-items: baseline; -} - -.content-center { - align-content: center; -} - -.items-start { - align-items: flex-start; -} - -.items-center { - align-items: center; -} - -.justify-center { - justify-content: center; -} - -.justify-between { - justify-content: space-between; -} - -.gap-1 { - gap: 0.25rem; -} - -.gap-2 { - gap: 0.5rem; -} - -.gap-4 { - gap: 1rem; -} - -.gap-6 { - 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)); - margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); -} - -.space-x-2 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(0.5rem * var(--tw-space-x-reverse)); - margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); -} - -.space-x-3 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(0.75rem * var(--tw-space-x-reverse)); - margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); -} - -.space-x-4 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(1rem * var(--tw-space-x-reverse)); - margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); -} - -.space-y-2 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); -} - -.space-y-3 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); -} - -.space-y-4 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(1rem * var(--tw-space-y-reverse)); -} - -.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)); -} - -.divide-y > :not([hidden]) ~ :not([hidden]) { - --tw-divide-y-reverse: 0; - border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); -} - -.divide-gray-200 > :not([hidden]) ~ :not([hidden]) { - --tw-divide-opacity: 1; - border-color: rgb(229 231 235 / var(--tw-divide-opacity, 1)); -} - -.overflow-auto { - overflow: auto; -} - -.overflow-hidden { - overflow: hidden; -} - -.overflow-scroll { - overflow: scroll; -} - -.overflow-x-auto { - overflow-x: auto; -} - -.hyphens-auto { - -webkit-hyphens: auto; - hyphens: auto; -} - -.whitespace-nowrap { - white-space: nowrap; -} - -.break-words { - overflow-wrap: break-word; -} - -.break-all { - word-break: break-all; -} - -.rounded { - border-radius: 0.25rem; -} - -.rounded-full { - border-radius: 9999px; -} - -.rounded-lg { - border-radius: 0.5rem; -} - -.rounded-md { - border-radius: 0.375rem; -} - -.rounded-sm { - border-radius: 0.125rem; -} - -.rounded-xl { - border-radius: 0.75rem; -} - -.border { - border-width: 1px; -} - -.border-b { - 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)); -} - -.border-gray-200 { - --tw-border-opacity: 1; - border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); -} - -.border-gray-300 { - --tw-border-opacity: 1; - border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); -} - -.border-green-200 { - --tw-border-opacity: 1; - border-color: rgb(187 247 208 / var(--tw-border-opacity, 1)); -} - -.border-purple-200 { - --tw-border-opacity: 1; - border-color: rgb(233 213 255 / var(--tw-border-opacity, 1)); -} - -.border-red-200 { - --tw-border-opacity: 1; - border-color: rgb(254 202 202 / var(--tw-border-opacity, 1)); -} - -.border-slate-300 { - --tw-border-opacity: 1; - border-color: rgb(203 213 225 / var(--tw-border-opacity, 1)); -} - -.border-transparent { - border-color: transparent; -} - -.border-yellow-200 { - --tw-border-opacity: 1; - border-color: rgb(254 240 138 / var(--tw-border-opacity, 1)); -} - -.bg-blue-100 { - --tw-bg-opacity: 1; - background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)); -} - -.bg-blue-50 { - --tw-bg-opacity: 1; - background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1)); -} - -.bg-blue-600 { - --tw-bg-opacity: 1; - background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); -} - -.bg-gray-100 { - --tw-bg-opacity: 1; - background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); -} - -.bg-gray-200 { - --tw-bg-opacity: 1; - background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); -} - -.bg-gray-50 { - --tw-bg-opacity: 1; - background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); -} - -.bg-green-50 { - --tw-bg-opacity: 1; - background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1)); -} - -.bg-margeblue { - --tw-bg-opacity: 1; - background-color: rgb(62 95 111 / var(--tw-bg-opacity, 1)); -} - -.bg-purple-50 { - --tw-bg-opacity: 1; - background-color: rgb(250 245 255 / var(--tw-bg-opacity, 1)); -} - -.bg-red-100 { - --tw-bg-opacity: 1; - background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1)); -} - -.bg-red-50 { - --tw-bg-opacity: 1; - background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)); -} - -.bg-slate-100 { - --tw-bg-opacity: 1; - background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1)); -} - -.bg-slate-50 { - --tw-bg-opacity: 1; - background-color: rgb(248 250 252 / var(--tw-bg-opacity, 1)); -} - -.bg-white { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); -} - -.bg-white\/90 { - background-color: rgb(255 255 255 / 0.9); -} - -.bg-yellow-100 { - --tw-bg-opacity: 1; - background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1)); -} - -.bg-yellow-50 { - --tw-bg-opacity: 1; - background-color: rgb(254 252 232 / var(--tw-bg-opacity, 1)); -} - -.p-1 { - padding: 0.25rem; -} - -.p-1\.5 { - padding: 0.375rem; -} - -.p-2 { - padding: 0.5rem; -} - -.p-2\.5 { - padding: 0.625rem; -} - -.p-3 { - padding: 0.75rem; -} - -.p-4 { - padding: 1rem; -} - -.p-5 { - padding: 1.25rem; -} - -.p-6 { - padding: 1.5rem; -} - -.px-1 { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -.px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; -} - -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} - -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} - -.py-1 { - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.py-3 { - padding-top: 0.75rem; - padding-bottom: 0.75rem; -} - -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.pb-4 { - padding-bottom: 1rem; -} - -.pb-6 { - padding-bottom: 1.5rem; -} - -.pl-2 { - padding-left: 0.5rem; -} - -.pl-4 { - padding-left: 1rem; -} - -.pr-4 { - padding-right: 1rem; -} - -.pt-2 { - padding-top: 0.5rem; -} - -.pt-4 { - padding-top: 1rem; -} - -.pt-5 { - padding-top: 1.25rem; -} - -.text-left { - text-align: left; -} - -.-indent-4 { - text-indent: -1rem; -} - -.font-mono { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} - -.font-sans { - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -} - -.font-serif { - font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; -} - -.text-base { - font-size: 1rem; - line-height: 1.5rem; -} - -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} - -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - -.font-medium { - font-weight: 500; -} - -.uppercase { - text-transform: uppercase; -} - -.leading-5 { - line-height: 1.25rem; -} - -.leading-6 { - line-height: 1.5rem; -} - -.leading-relaxed { - line-height: 1.625; -} - -.tracking-wider { - letter-spacing: 0.05em; -} - -.text-black { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity, 1)); -} - -.text-blue-600 { - --tw-text-opacity: 1; - color: rgb(37 99 235 / var(--tw-text-opacity, 1)); -} - -.text-blue-700 { - --tw-text-opacity: 1; - color: rgb(29 78 216 / var(--tw-text-opacity, 1)); -} - -.text-blue-800 { - --tw-text-opacity: 1; - color: rgb(30 64 175 / var(--tw-text-opacity, 1)); -} - -.text-blue-900 { - --tw-text-opacity: 1; - color: rgb(30 58 138 / var(--tw-text-opacity, 1)); -} - -.text-blue-950 { - --tw-text-opacity: 1; - color: rgb(23 37 84 / var(--tw-text-opacity, 1)); -} - -.text-gray-500 { - --tw-text-opacity: 1; - color: rgb(107 114 128 / var(--tw-text-opacity, 1)); -} - -.text-gray-600 { - --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity, 1)); -} - -.text-gray-700 { - --tw-text-opacity: 1; - color: rgb(55 65 81 / var(--tw-text-opacity, 1)); -} - -.text-gray-800 { - --tw-text-opacity: 1; - color: rgb(31 41 55 / var(--tw-text-opacity, 1)); -} - -.text-gray-900 { - --tw-text-opacity: 1; - color: rgb(17 24 39 / var(--tw-text-opacity, 1)); -} - -.text-green-800 { - --tw-text-opacity: 1; - color: rgb(22 101 52 / var(--tw-text-opacity, 1)); -} - -.text-green-900 { - --tw-text-opacity: 1; - color: rgb(20 83 45 / var(--tw-text-opacity, 1)); -} - -.text-liteblue { - --tw-text-opacity: 1; - color: rgb(0 102 204 / var(--tw-text-opacity, 1)); -} - -.text-margeblue { - --tw-text-opacity: 1; - color: rgb(62 95 111 / var(--tw-text-opacity, 1)); -} - -.text-orange-500 { - --tw-text-opacity: 1; - color: rgb(249 115 22 / var(--tw-text-opacity, 1)); -} - -.text-purple-800 { - --tw-text-opacity: 1; - color: rgb(107 33 168 / var(--tw-text-opacity, 1)); -} - -.text-purple-900 { - --tw-text-opacity: 1; - color: rgb(88 28 135 / var(--tw-text-opacity, 1)); -} - -.text-red-700 { - --tw-text-opacity: 1; - color: rgb(185 28 28 / var(--tw-text-opacity, 1)); -} - -.text-red-800 { - --tw-text-opacity: 1; - color: rgb(153 27 27 / var(--tw-text-opacity, 1)); -} - -.text-red-900 { - --tw-text-opacity: 1; - color: rgb(127 29 29 / var(--tw-text-opacity, 1)); -} - -.text-slate-600 { - --tw-text-opacity: 1; - color: rgb(71 85 105 / var(--tw-text-opacity, 1)); -} - -.text-slate-700 { - --tw-text-opacity: 1; - color: rgb(51 65 85 / var(--tw-text-opacity, 1)); -} - -.text-slate-800 { - --tw-text-opacity: 1; - color: rgb(30 41 59 / var(--tw-text-opacity, 1)); -} - -.text-slate-900 { - --tw-text-opacity: 1; - color: rgb(15 23 42 / var(--tw-text-opacity, 1)); -} - -.text-white { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity, 1)); -} - -.text-yellow-700 { - --tw-text-opacity: 1; - color: rgb(161 98 7 / var(--tw-text-opacity, 1)); -} - -.text-yellow-900 { - --tw-text-opacity: 1; - color: rgb(113 63 18 / var(--tw-text-opacity, 1)); -} - -.underline { - text-decoration-line: underline; -} - -.shadow { - --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px 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-inner { - --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: inset 0 2px 4px 0 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-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); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-sm { - --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); - 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); -} - -.backdrop-blur-sm { - --tw-backdrop-blur: blur(4px); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -} - -.hover\:bg-blue-700:hover { - --tw-bg-opacity: 1; - background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); -} - -.hover\:bg-gray-100:hover { - --tw-bg-opacity: 1; - background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); -} - -.hover\:bg-gray-50:hover { - --tw-bg-opacity: 1; - background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); -} - -.hover\:text-blue-800:hover { - --tw-text-opacity: 1; - color: rgb(30 64 175 / var(--tw-text-opacity, 1)); -} - -.hover\:text-gray-900:hover { - --tw-text-opacity: 1; - color: rgb(17 24 39 / var(--tw-text-opacity, 1)); -} - -.hover\:text-liteblue:hover { - --tw-text-opacity: 1; - color: rgb(0 102 204 / var(--tw-text-opacity, 1)); -} - -.hover\:text-red-600:hover { - --tw-text-opacity: 1; - color: rgb(220 38 38 / var(--tw-text-opacity, 1)); -} - -.hover\:text-slate-200:hover { - --tw-text-opacity: 1; - color: rgb(226 232 240 / var(--tw-text-opacity, 1)); -} - -.hover\:underline:hover { - text-decoration-line: underline; -} - -.focus\:outline-none:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.focus\:ring-2:focus { - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); -} - -.focus\:ring-blue-500:focus { - --tw-ring-opacity: 1; - --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1)); -} - -.focus\:ring-offset-2:focus { - --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)); -} - -.has-\[\:checked\]\:bg-slate-200:has(:checked) { - --tw-bg-opacity: 1; - background-color: rgb(226 232 240 / var(--tw-bg-opacity, 1)); -} - -.has-\[\:checked\]\:text-slate-900:has(:checked) { - --tw-text-opacity: 1; - color: rgb(15 23 42 / var(--tw-text-opacity, 1)); -} - -.has-\[\:checked\]\:underline:has(:checked) { - text-decoration-line: underline; -} - -@media (min-width: 440px) { - .xs\:gap-2 { - gap: 0.5rem; - } -} - -@media (min-width: 640px) { - .sm\:m-0 { - margin: 0px; - } - - .sm\:mr-3 { - margin-right: 0.75rem; - } - - .sm\:block { - display: block; - } - - .sm\:grid { - display: grid; - } - - .sm\:hidden { - display: none; - } - - .sm\:grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .sm\:flex-row { - flex-direction: row; - } - - .sm\:space-x-2 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(0.5rem * var(--tw-space-x-reverse)); - margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); - } - - .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(0px * var(--tw-space-y-reverse)); - } - - .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\:p-4 { - padding: 1rem; - } - - .sm\:px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; - } - - .sm\:py-4 { - padding-top: 1rem; - padding-bottom: 1rem; - } - - .sm\:text-xl { - font-size: 1.25rem; - line-height: 1.75rem; - } -} - -@media (min-width: 768px) { - .md\:my-4 { - margin-top: 1rem; - margin-bottom: 1rem; - } - - .md\:ml-16 { - margin-left: 4rem; - } - - .md\:mr-16 { - margin-right: 4rem; - } - - .md\:mr-8 { - margin-right: 2rem; - } - - .md\:block { - display: block; - } - - .md\:inline { - display: inline; - } - - .md\:hidden { - display: none; - } - - .md\:w-32 { - width: 8rem; - } - - .md\:w-64 { - width: 16rem; - } - - .md\:w-auto { - width: auto; - } - - .md\:max-w-lg { - max-width: 32rem; - } - - .md\:grid-cols-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - .md\:flex-row { - flex-direction: row; - } - - .md\:place-items-start { - place-items: start; - } - - .md\:place-items-baseline { - place-items: baseline; - } - - .md\:gap-8 { - gap: 2rem; - } - - .md\:space-x-2 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(0.5rem * var(--tw-space-x-reverse)); - margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); - } - - .md\:space-x-8 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(2rem * var(--tw-space-x-reverse)); - 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; - } - - .md\:py-4 { - padding-top: 1rem; - padding-bottom: 1rem; - } - - .md\:text-xl { - font-size: 1.25rem; - line-height: 1.75rem; - } -} - -@media (min-width: 1280px) { - .xl\:grid-cols-4 { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } -} - -.\[\&\:not\(\:last-child\)\]\:border-b:not(:last-child) { - border-bottom-width: 1px; -} \ No newline at end of file diff --git a/code/services-application/search-service/resources/static/search/rss.svg b/code/services-application/search-service/resources/static/search/rss.svg deleted file mode 100644 index 2c01c8b3..00000000 --- a/code/services-application/search-service/resources/static/search/rss.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/code/services-application/search-service/resources/static/search/serp.scss b/code/services-application/search-service/resources/static/search/serp.scss deleted file mode 100644 index ea4adcd0..00000000 --- a/code/services-application/search-service/resources/static/search/serp.scss +++ /dev/null @@ -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; -} diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-brands-400.ttf b/code/services-application/search-service/resources/static/webfonts/fa-brands-400.ttf similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-brands-400.ttf rename to code/services-application/search-service/resources/static/webfonts/fa-brands-400.ttf diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-brands-400.woff2 b/code/services-application/search-service/resources/static/webfonts/fa-brands-400.woff2 similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-brands-400.woff2 rename to code/services-application/search-service/resources/static/webfonts/fa-brands-400.woff2 diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-regular-400.ttf b/code/services-application/search-service/resources/static/webfonts/fa-regular-400.ttf similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-regular-400.ttf rename to code/services-application/search-service/resources/static/webfonts/fa-regular-400.ttf diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-regular-400.woff2 b/code/services-application/search-service/resources/static/webfonts/fa-regular-400.woff2 similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-regular-400.woff2 rename to code/services-application/search-service/resources/static/webfonts/fa-regular-400.woff2 diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-solid-900.ttf b/code/services-application/search-service/resources/static/webfonts/fa-solid-900.ttf similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-solid-900.ttf rename to code/services-application/search-service/resources/static/webfonts/fa-solid-900.ttf diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-solid-900.woff2 b/code/services-application/search-service/resources/static/webfonts/fa-solid-900.woff2 similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-solid-900.woff2 rename to code/services-application/search-service/resources/static/webfonts/fa-solid-900.woff2 diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-v4compatibility.ttf b/code/services-application/search-service/resources/static/webfonts/fa-v4compatibility.ttf similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-v4compatibility.ttf rename to code/services-application/search-service/resources/static/webfonts/fa-v4compatibility.ttf diff --git a/code/services-application/search-service/resources/static/search/webfonts/fa-v4compatibility.woff2 b/code/services-application/search-service/resources/static/webfonts/fa-v4compatibility.woff2 similarity index 100% rename from code/services-application/search-service/resources/static/search/webfonts/fa-v4compatibility.woff2 rename to code/services-application/search-service/resources/static/webfonts/fa-v4compatibility.woff2 diff --git a/code/services-application/search-service/test/nu/marginalia/search/command/commands/BangCommandTest.java b/code/services-application/search-service/test/nu/marginalia/search/command/commands/BangCommandTest.java deleted file mode 100644 index 6be5690e..00000000 --- a/code/services-application/search-service/test/nu/marginalia/search/command/commands/BangCommandTest.java +++ /dev/null @@ -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"); - } - -} \ No newline at end of file diff --git a/code/services-application/status-service/java/nu/marginalia/status/StatusService.java b/code/services-application/status-service/java/nu/marginalia/status/StatusService.java index 4a7dfad6..59ea0af9 100644 --- a/code/services-application/status-service/java/nu/marginalia/status/StatusService.java +++ b/code/services-application/status-service/java/nu/marginalia/status/StatusService.java @@ -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); diff --git a/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java b/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java index 2b2688f7..4271d7c2 100644 --- a/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java +++ b/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java @@ -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; diff --git a/code/services-core/control-service/java/nu/marginalia/control/ControlService.java b/code/services-core/control-service/java/nu/marginalia/control/ControlService.java index 64aaa133..6cf55628 100644 --- a/code/services-core/control-service/java/nu/marginalia/control/ControlService.java +++ b/code/services-core/control-service/java/nu/marginalia/control/ControlService.java @@ -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(); diff --git a/code/services-core/executor-service/java/nu/marginalia/executor/ExecutorSvc.java b/code/services-core/executor-service/java/nu/marginalia/executor/ExecutorSvc.java index b47505cc..de08c3e6 100644 --- a/code/services-core/executor-service/java/nu/marginalia/executor/ExecutorSvc.java +++ b/code/services-core/executor-service/java/nu/marginalia/executor/ExecutorSvc.java @@ -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; diff --git a/code/services-core/index-service/java/nu/marginalia/index/IndexService.java b/code/services-core/index-service/java/nu/marginalia/index/IndexService.java index cbd2c6a3..3c86020c 100644 --- a/code/services-core/index-service/java/nu/marginalia/index/IndexService.java +++ b/code/services-core/index-service/java/nu/marginalia/index/IndexService.java @@ -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 diff --git a/code/services-core/query-service/java/nu/marginalia/query/QueryService.java b/code/services-core/query-service/java/nu/marginalia/query/QueryService.java index 4b69d454..1e612226 100644 --- a/code/services-core/query-service/java/nu/marginalia/query/QueryService.java +++ b/code/services-core/query-service/java/nu/marginalia/query/QueryService.java @@ -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); diff --git a/settings.gradle b/settings.gradle index 051755b6..04f94630 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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'])