diff --git a/.github/publish-coverage-badge.sh b/.github/publish-coverage-badge.sh index 7926055f..9facbb38 100755 --- a/.github/publish-coverage-badge.sh +++ b/.github/publish-coverage-badge.sh @@ -3,7 +3,7 @@ # shields.io reads, on the badges branch so that no outside service or secret is needed. set -eu -reports="core/target/site/jacoco/jacoco.csv web/target/site/jacoco/jacoco.csv ui/target/site/jacoco/jacoco.csv" +reports="core/target/site/jacoco/jacoco.csv ui/target/site/jacoco/jacoco.csv" # Columns four and five of a JaCoCo csv are the instructions missed and covered; the first line is the header. percent=$(awk -F, 'FNR > 1 { missed += $4; covered += $5 } END { printf "%d", 100 * covered / (missed + covered) }' $reports) diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml index 12859215..0a2365d3 100644 --- a/.github/workflows/installers.yml +++ b/.github/workflows/installers.yml @@ -17,7 +17,6 @@ on: - "desktop/**" - "shell/**" - "ui/**" - - "web/**" - "core/**" - "pom.xml" - ".github/workflows/installers.yml" @@ -28,7 +27,6 @@ on: - "desktop/**" - "shell/**" - "ui/**" - - "web/**" - "core/**" - "pom.xml" - ".github/workflows/installers.yml" diff --git a/README.md b/README.md index 6e80760a..2782ce01 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,17 @@ The `master` branch is regularly built and tested, but is not guaranteed to be c mvn clean install ``` -This builds and tests the `core`, `web`, `ui` and `shell` modules: the Hedgehog -client, the embedded web server, the server-rendered pages and the window that -hosts them. +This builds and tests the `core`, `ui` and `shell` modules: the Hedgehog +client, the server-rendered pages and the start of the application. The embedded web +server and the window that hosts it come from [Oriel](https://github.com/unigrid-project/oriel), +which the build fetches from JitPack at the release `oriel.version` names in the root pom. + +To build against a checkout of Oriel instead, such as a change that is not released +yet, run `mvn install` in that checkout and name the coordinates it installs as, with +the version from Oriel's own pom: +``` +mvn clean install -Doriel.groupId=org.unigrid.oriel -Doriel.version=0.0.2-SNAPSHOT +``` The `desktop` module builds the native installers and is only part of the build with the `installer` profile. It needs JDK 25 and the platform's packaging tools diff --git a/desktop/src/test/java/org/unigrid/janus/desktop/InstalledJanusIT.java b/desktop/src/test/java/org/unigrid/janus/desktop/InstalledJanusIT.java index ca5384ce..0aaaad6b 100644 --- a/desktop/src/test/java/org/unigrid/janus/desktop/InstalledJanusIT.java +++ b/desktop/src/test/java/org/unigrid/janus/desktop/InstalledJanusIT.java @@ -31,7 +31,7 @@ of the License (see COPYING and COPYING.addendum). import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; -import org.unigrid.janus.shell.BrowserWindow; +import org.unigrid.oriel.window.BrowserWindow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; @@ -180,8 +180,8 @@ private static Container startOnWayland(final WaylandDesktop desktop) throws IOE ls /tmp/.X11-unix/X* >/dev/null 2>&1 || { cat /tmp/weston.log; exit 1; } %2$s XDG_SESSION_TYPE=wayland WAYLAND_DISPLAY=wayland-1 XDG_CURRENT_DESKTOP=%3$s \ JAVA_TOOL_OPTIONS=-D%4$s=9222 setsid nohup %5$s/bin/Unigrid >%6$s 2>&1 & - """.formatted(USER, XWAYLAND_SCREEN, desktop.currentDesktop, BrowserWindow.DEBUGGING_PORT, - PACKAGED, LOG), + """.formatted(USER, XWAYLAND_SCREEN, desktop.currentDesktop, + BrowserWindow.DEBUGGING_PORT, PACKAGED, LOG), "starting Janus on Wayland"); awaitAddress(container); return container; diff --git a/desktop/src/test/java/org/unigrid/janus/desktop/RunningJanus.java b/desktop/src/test/java/org/unigrid/janus/desktop/RunningJanus.java index b88a0170..65dcede0 100644 --- a/desktop/src/test/java/org/unigrid/janus/desktop/RunningJanus.java +++ b/desktop/src/test/java/org/unigrid/janus/desktop/RunningJanus.java @@ -43,7 +43,7 @@ of the License (see COPYING and COPYING.addendum). import java.util.stream.Stream; import javax.imageio.ImageIO; import org.unigrid.janus.core.hedgehog.HedgehogService; -import org.unigrid.janus.shell.BrowserWindow; +import org.unigrid.oriel.window.BrowserWindow; /** * The installed Janus with its window open to a debugging connection, so a test can work it the way a person diff --git a/desktop/src/test/java/org/unigrid/janus/desktop/StaticFilesProbe.java b/desktop/src/test/java/org/unigrid/janus/desktop/StaticFilesProbe.java index 8bc92a6b..8c07128b 100644 --- a/desktop/src/test/java/org/unigrid/janus/desktop/StaticFilesProbe.java +++ b/desktop/src/test/java/org/unigrid/janus/desktop/StaticFilesProbe.java @@ -23,13 +23,14 @@ of the License (see COPYING and COPYING.addendum). import java.net.http.HttpResponse; import java.util.List; import java.util.function.Supplier; -import org.unigrid.janus.web.Routes; -import org.unigrid.janus.web.SessionToken; -import org.unigrid.janus.web.Templates; -import org.unigrid.janus.web.UiServer; -import org.unigrid.janus.web.WindowControl; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.AppId; +import org.unigrid.oriel.server.Routes; +import org.unigrid.oriel.server.SessionToken; +import org.unigrid.oriel.server.UiServer; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.action.View; +import org.unigrid.oriel.server.template.Templates; +import org.unigrid.oriel.server.window.WindowControl; /** * Serves the interface from whatever Java runs it, asks for the files the pages are drawn with, and prints the @@ -38,7 +39,7 @@ of the License (see COPYING and COPYING.addendum). */ public final class StaticFilesProbe { static final List ASSETS = List.of( - "/static/css/janus.css", "/static/js/htmx.min.js", "/static/fonts/sora-latin.woff2" + "/static/css/janus.css", "/oriel/htmx.min.js", "/static/fonts/sora-latin.woff2" ); private StaticFilesProbe() { @@ -49,8 +50,8 @@ public static void main(final String[] arguments) throws Exception { throw new IllegalStateException("The request reached the page instead of being served as a file"); }; final SessionToken token = SessionToken.random(); - final URI base = new UiServer(Routes.create(new Templates(false), token, WindowControl.NONE, Actions.of(), - noPage)).start(); + final URI base = new UiServer(Routes.create(AppId.of("janus"), new Templates(false), token, + WindowControl.NONE, Actions.of(), noPage)).start(); final HttpClient http = HttpClient.newBuilder().cookieHandler(new CookieManager()) .followRedirects(HttpClient.Redirect.NORMAL).build(); diff --git a/e2e/pom.xml b/e2e/pom.xml index 97aa538f..5dbc29e4 100644 --- a/e2e/pom.xml +++ b/e2e/pom.xml @@ -25,9 +25,8 @@ test - org.unigrid.janus - web - ${project.version} + ${oriel.groupId} + oriel-server test-jar test diff --git a/pom.xml b/pom.xml index 3d4c384c..e0c8d0bc 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,6 @@ core - web ui shell @@ -38,8 +37,10 @@ UTF-8 25 1.18.48 - 12.0.21 - 3.1.3.RELEASE + + com.github.unigrid-project.oriel + v0.0.1 146.0.10 4.7.7 5.1.2.Final @@ -97,16 +98,21 @@ - org.eclipse.jetty - jetty-bom - ${jetty.version} - pom - import + ${oriel.groupId} + oriel-server + ${oriel.version} - org.thymeleaf - thymeleaf - ${thymeleaf.version} + ${oriel.groupId} + oriel-server + ${oriel.version} + test-jar + test + + + ${oriel.groupId} + oriel-window + ${oriel.version} me.friwi @@ -197,6 +203,12 @@ + + + jitpack + https://jitpack.io + + diff --git a/shell/pom.xml b/shell/pom.xml index aaeaf417..a54697bd 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -9,7 +9,7 @@ shell jar Unigrid Janus Shell - Desktop window hosting the embedded browser, the system tray and the application lifecycle. + Starts Janus: the container, the interface server and the window that shows it. org.unigrid.janus @@ -17,13 +17,8 @@ ${project.version} - org.projectlombok - lombok - provided - - - me.friwi - jcefmaven + ${oriel.groupId} + oriel-window info.picocli diff --git a/shell/src/main/java/org/unigrid/janus/shell/BrowserWindow.java b/shell/src/main/java/org/unigrid/janus/shell/BrowserWindow.java deleted file mode 100644 index 45a3bdf9..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/BrowserWindow.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.net.URI; -import java.time.Duration; -import javax.swing.JFrame; -import javax.swing.SwingUtilities; -import me.friwi.jcefmaven.CefAppBuilder; -import me.friwi.jcefmaven.MavenCefAppHandlerAdapter; -import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler; -import org.cef.CefApp; -import org.cef.CefClient; -import org.cef.browser.CefBrowser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.unigrid.janus.web.WindowControl; - -public class BrowserWindow { - public static final String DEBUGGING_PORT = "janus.remote-debugging-port"; - - private static final Logger LOG = LoggerFactory.getLogger(BrowserWindow.class); - private static final Dimension SIZE = new Dimension(1240, 800); - private static final Duration SHUTDOWN_GRACE = Duration.ofSeconds(10); - private static final String TITLE = "Unigrid"; - - private final JFrame frame = new JFrame(TITLE); - private final FrameControl control = new FrameControl(frame); - private final WindowSystem windowSystem; - - public BrowserWindow(final WindowSystem windowSystem) { - this.windowSystem = windowSystem; - - /* The title bar and its buttons are drawn by the page, so the frame contributes - nothing but its outline. Decoration has to be settled before the frame is - realised, which is why it happens here rather than alongside the sizing. */ - frame.setUndecorated(true); - frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - frame.setIconImages(WindowIcon.images()); - fitShapeToFrame(); - } - - private void fitShapeToFrame() { - if (!shapedWindowsSupported()) { - return; - } - - /* The state changes from the window manager as well as from the page, and the - resize that comes with it is not ordered against the state change, so the - shape follows both. */ - frame.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(final ComponentEvent event) { - fitShape(); - } - }); - frame.addWindowStateListener(event -> fitShape()); - } - - private void fitShape() { - frame.setShape(WindowShape.of(frame.getExtendedState(), frame.getSize()).orElse(null)); - } - - private boolean shapedWindowsSupported() { - return frame.getGraphicsConfiguration().getDevice().isWindowTranslucencySupported( - GraphicsDevice.WindowTranslucency.PERPIXEL_TRANSPARENT - ); - } - - public WindowControl control() { - return control; - } - - public void open(final URI uri) throws Exception { - final CefAppBuilder builder = new CefAppBuilder(); - final CefLocation location = new CefLocation(); - - builder.setInstallDir(location.directory()); - - /* The folder of an installed application is not ours to write to, so a missing or damaged engine - there has to fail with the engine's own error rather than trigger a download or a deletion. */ - builder.setSkipInstallation(location.isBundled()); - builder.setProgressHandler(new ConsoleProgressHandler()); - builder.getCefSettings().windowless_rendering_enabled = false; - - if (WindowSystem.CHOOSABLE) { - builder.addJcefArgs(windowSystem.engineSwitch()); - } - - /* Lets the installer tests work the installed wallet's window; nothing listens unless a port is named. */ - final Integer debuggingPort = Integer.getInteger(DEBUGGING_PORT); - - if (debuggingPort != null) { - builder.getCefSettings().remote_debugging_port = debuggingPort; - } - - /* Chromium keeps running after the last window closes, so the process has to be - torn down explicitly or the wallet lingers with no way to reach it. */ - builder.setAppHandler(new MavenCefAppHandlerAdapter() { - @Override - public void stateHasChanged(final CefApp.CefAppState state) { - LOG.info("The browser engine is {}", state); - - if (state == CefApp.CefAppState.TERMINATED) { - System.exit(0); - } - } - }); - - final CefClient client = builder.build().createClient(); - final CefBrowser browser = client.createBrowser(uri.toString(), false, false); - - frame.addWindowListener(shutDownOnClose(browser)); - - /* The browser has to be in the content pane before the frame is shown. Attaching it - afterwards leaves Chromium to open a top level window of its own, and the frame - stays empty. */ - frame.getContentPane().add(browser.getUIComponent(), BorderLayout.CENTER); - - /* The browser engine creates the browser the first time its component is painted. On Windows the - engine's own native child covers the whole of the undecorated frame, which leaves Windows nothing - to ask the frame to paint, so the component is asked to paint itself. */ - SwingUtilities.invokeLater(() -> { - /* The work area leaves out the taskbar, and on a scaled display it is counted in the same - logical pixels as the size, so a small or scaled screen still shows the whole window. */ - frame.setBounds(OpeningBounds.centred(SIZE, - GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds())); - frame.setVisible(true); - browser.getUIComponent().repaint(); - }); - } - - /* Chromium answers a close request by asking the window to close and cancels its own - unless the browser has been told that closing is allowed, so without that permission - the browser, and with it the process, lives on. The frame is hidden rather than - disposed because the shutdown completes on the event thread, which AWT retires once - the last window is gone. On macOS the engine now and then never finishes shutting - down, which would leave Janus running with no window, so it gets a deadline. */ - private WindowAdapter shutDownOnClose(final CefBrowser browser) { - return new WindowAdapter() { - @Override - public void windowClosing(final WindowEvent event) { - LOG.info("The window is closing"); - frame.setVisible(false); - browser.setCloseAllowed(); - CefApp.getInstance().dispose(); - ShutdownDeadline.start(SHUTDOWN_GRACE, () -> { - LOG.warn("The browser engine did not shut down in time; ending anyway"); - System.exit(0); - }); - } - }; - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/CefLocation.java b/shell/src/main/java/org/unigrid/janus/shell/CefLocation.java deleted file mode 100644 index 72d02d68..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/CefLocation.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.io.File; -import java.nio.file.Path; -import java.util.Objects; - -/** - * Where the browser engine lives. An installed Janus carries it inside the application folder and names that - * folder through a property; a development run has no such property and keeps the engine in the home folder. - */ -public class CefLocation { - public static final String PROPERTY = "janus.jcef"; - private static final String HOME_FOLDER = ".janus/jcef"; - - private final String bundled; - private final Path home; - - public CefLocation() { - this(System.getProperty(PROPERTY), Path.of(System.getProperty("user.home"))); - } - - CefLocation(final String bundled, final Path home) { - this.bundled = bundled; - this.home = home; - } - - public boolean isBundled() { - return Objects.nonNull(bundled) && !bundled.isBlank(); - } - - public File directory() { - return isBundled() ? new File(bundled) : home.resolve(HOME_FOLDER).toFile(); - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/OpeningBounds.java b/shell/src/main/java/org/unigrid/janus/shell/EngineFailure.java similarity index 59% rename from shell/src/main/java/org/unigrid/janus/shell/OpeningBounds.java rename to shell/src/main/java/org/unigrid/janus/shell/EngineFailure.java index a60007fa..978cef8f 100644 --- a/shell/src/main/java/org/unigrid/janus/shell/OpeningBounds.java +++ b/shell/src/main/java/org/unigrid/janus/shell/EngineFailure.java @@ -16,19 +16,17 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.shell; -import java.awt.Dimension; -import java.awt.Rectangle; - -/** Where a window first appears: at the size it prefers, unless the screen has less room than that. */ -public final class OpeningBounds { - private OpeningBounds() { +import org.unigrid.oriel.window.Application; +import org.unigrid.oriel.window.StartFailure; +import org.unigrid.oriel.window.engine.CefLocation; + +/* What Janus tells a person whose browser engine would not start. It names the folder the engine was looked for + in: the installer's when Janus was installed, the home folder when it was not. */ +final class EngineFailure { + private EngineFailure() { } - public static Rectangle centred(final Dimension preferred, final Rectangle workArea) { - final int width = Math.min(preferred.width, workArea.width); - final int height = Math.min(preferred.height, workArea.height); - - return new Rectangle(workArea.x + (workArea.width - width) / 2, workArea.y + (workArea.height - height) / 2, - width, height); + static String message(final Application application, final Throwable cause) { + return StartFailure.message(application, new CefLocation(application.id()).directory(), cause); } } diff --git a/shell/src/main/java/org/unigrid/janus/shell/FrameControl.java b/shell/src/main/java/org/unigrid/janus/shell/FrameControl.java deleted file mode 100644 index ebd2a804..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/FrameControl.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Dimension; -import java.awt.FileDialog; -import java.awt.Frame; -import java.awt.MouseInfo; -import java.awt.Point; -import java.awt.PointerInfo; -import java.awt.Rectangle; -import java.awt.event.WindowEvent; -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Supplier; -import javax.swing.JFrame; -import javax.swing.SwingUtilities; -import javax.swing.Timer; -import org.unigrid.janus.web.WindowControl; - -public class FrameControl implements WindowControl { - private static final int FOLLOW_INTERVAL = 10; - private static final Dimension MINIMUM = new Dimension(900, 640); - - private final JFrame frame; - private final Timer follow = new Timer(FOLLOW_INTERVAL, event -> follow()); - private final AtomicBoolean asking = new AtomicBoolean(); - - private Consumer tracking; - - public FrameControl(final JFrame frame) { - this.frame = frame; - } - - @Override - public void minimise() { - onSwingThread(() -> frame.setExtendedState(frame.getExtendedState() | Frame.ICONIFIED)); - } - - @Override - public void toggleMaximise() { - onSwingThread(() -> frame.setExtendedState(maximised() ? Frame.NORMAL : Frame.MAXIMIZED_BOTH)); - } - - @Override - public void close() { - /* Routed through the window listener so that closing from the page tears Chromium - down the same way closing from the desktop does. */ - onSwingThread(() -> frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING))); - } - - @Override - public void beginMove() { - onSwingThread(() -> { - final Point pointer = pointer(); - - if (pointer == null) { - return; - } - - /* A maximised window cannot be dragged anywhere useful, so pulling on the - title bar restores it first, the way a native one behaves. */ - if (maximised()) { - frame.setExtendedState(Frame.NORMAL); - } - - final Point grip = new Point(pointer.x - frame.getX(), pointer.y - frame.getY()); - - track(at -> frame.setLocation(at.x - grip.x, at.y - grip.y)); - }); - } - - @Override - public void endMove() { - onSwingThread(follow::stop); - } - - @Override - public void beginResize(final Edge edge) { - onSwingThread(() -> { - final Point pointer = pointer(); - - if (pointer == null || maximised()) { - return; - } - - final Rectangle origin = frame.getBounds(); - - track(at -> frame.setBounds( - ResizeBounds.resized(edge, origin, new Point(at.x - pointer.x, at.y - pointer.y), MINIMUM) - )); - }); - } - - @Override - public void endResize() { - onSwingThread(follow::stop); - } - - @Override - public Optional chooseFile(final String title) { - return askHost(() -> ask(title, FileDialog.LOAD, null)); - } - - @Override - public Optional saveFile(final String title, final String name) { - return askHost(() -> ask(title, FileDialog.SAVE, name)); - } - - /* The dialog is modal to the frame but not to Chromium's window inside it, so a second - request while one is open would stack a second dialog; it is answered empty instead. */ - private Optional askHost(final Supplier dialog) { - if (!asking.compareAndSet(false, true)) { - return Optional.empty(); - } - - final AtomicReference picked = new AtomicReference<>(new File[0]); - - try { - SwingUtilities.invokeAndWait(() -> picked.set(dialog.get())); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (InvocationTargetException e) { - throw new IllegalStateException("The file dialog could not be shown", e.getCause()); - } finally { - asking.set(false); - } - - return Arrays.stream(picked.get()).findFirst().map(File::toPath); - } - - private File[] ask(final String title, final int mode, final String name) { - final FileDialog dialog = new FileDialog(frame, title, mode); - - dialog.setFile(name); - - try { - dialog.setVisible(true); - return dialog.getFiles(); - } finally { - dialog.dispose(); - } - } - - private void track(final Consumer placement) { - tracking = placement; - follow.start(); - } - - private void follow() { - final Point pointer = pointer(); - - if (pointer != null) { - tracking.accept(pointer); - } - } - - private boolean maximised() { - return (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH; - } - - private Point pointer() { - final PointerInfo info = MouseInfo.getPointerInfo(); - - /* Absent whenever the pointer is on another screen or the display cannot be - queried, in which case there is nothing sensible to move towards. */ - return info == null ? null : info.getLocation(); - } - - private void onSwingThread(final Runnable action) { - SwingUtilities.invokeLater(action); - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/Janus.java b/shell/src/main/java/org/unigrid/janus/shell/Janus.java index 67ebf348..bdcd80a2 100644 --- a/shell/src/main/java/org/unigrid/janus/shell/Janus.java +++ b/shell/src/main/java/org/unigrid/janus/shell/Janus.java @@ -18,6 +18,7 @@ of the License (see COPYING and COPYING.addendum). import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; +import java.awt.Dimension; import java.awt.GraphicsEnvironment; import java.net.URI; import java.nio.file.Path; @@ -27,13 +28,20 @@ of the License (see COPYING and COPYING.addendum). import org.slf4j.LoggerFactory; import org.unigrid.janus.ui.controller.WalletController; import org.unigrid.janus.ui.view.IndexView; -import org.unigrid.janus.web.Routes; -import org.unigrid.janus.web.action.ActionExtension; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.SessionToken; -import org.unigrid.janus.web.Templates; -import org.unigrid.janus.web.UiServer; +import org.unigrid.oriel.server.AppId; +import org.unigrid.oriel.server.Routes; +import org.unigrid.oriel.server.SessionToken; +import org.unigrid.oriel.server.UiServer; +import org.unigrid.oriel.server.action.ActionExtension; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.template.Templates; +import org.unigrid.oriel.window.Application; +import org.unigrid.oriel.window.BrowserWindow; +import org.unigrid.oriel.window.WindowIcon; +import org.unigrid.oriel.window.launch.LaunchOptions; import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; public final class Janus { /* Logging starts when the logger below is made, and writes its files to the folder this makes. */ @@ -49,30 +57,74 @@ private Janus() { } public static void main(final String[] args) throws Exception { + final CommandLine command = command(); + final Optional options; + try { - final Optional options = LaunchOptions.of(args); - - if (options.isPresent()) { - start(options.get()); - } - } catch (CommandLine.ParameterException e) { - System.err.println(e.getMessage()); - e.getCommandLine().usage(System.err); + options = options(command, args); + } catch (IllegalArgumentException | CommandLine.ParameterException e) { + refuse(command, e); System.exit(USAGE_ERROR); + return; + } + + if (options.isPresent()) { + start(application(), options.get()); + } + } + + /* + * The command line is Janus's: Oriel takes out the window system and Janus reads what is left, refusing what + * it does not know so that a mistyped option is never quietly ignored. Empty when the help was asked for, + * which is printed plainly on the command's own output; an option Janus or Oriel does not take is thrown, + * so that main alone decides how Janus ends. + */ + static Optional options(final CommandLine command, final String... args) { + final LaunchOptions options = LaunchOptions.of(args); + + if (command.parseArgs(options.remaining().toArray(String[]::new)).isUsageHelpRequested()) { + command.usage(command.getOut()); + command.getOut().flush(); + return Optional.empty(); } + + return Optional.of(options); } - private static void start(final LaunchOptions options) throws Exception { + /* A refused command line is answered on the command's error output, the way picocli answers it itself. */ + static void refuse(final CommandLine command, final RuntimeException refused) { + command.getErr().println(refused.getMessage()); + command.usage(command.getErr()); + command.getErr().flush(); + } + + /* What Janus takes, with the window system Oriel takes listed in the help where it can be chosen. */ + static CommandLine command() { + final CommandLine command = new CommandLine(new Arguments()); + + LaunchOptions.help().ifPresent(line -> command.getCommandSpec().usageMessage().footer("", "Window:", + " " + line)); + return command; + } + + /* The window and the message shown when it cannot open both go by this name. */ + private static Application application() { + return new Application(AppId.of("janus"), "Unigrid", WindowIcon.images(Janus.class, "icons/unigrid-%d.png"), + new Dimension(1240, 800), new Dimension(900, 640) + ); + } + + private static void start(final Application application, final LaunchOptions options) throws Exception { final SeContainer container = SeContainerInitializer.newInstance().initialize(); final Actions actions = Actions.discovered( container.getBeanManager().getExtension(ActionExtension.class) ); - final BrowserWindow window = new BrowserWindow(options.windowSystem()); + final BrowserWindow window = new BrowserWindow(application, options.windowSystem()); final SessionToken token = SessionToken.random(); final WalletController wallet = container.select(WalletController.class).get(); - final UiServer server = new UiServer(Routes.create(new Templates(false), token, window.control(), actions, - () -> new IndexView(TITLE, wallet.start()) + final UiServer server = new UiServer(Routes.create(application.id(), new Templates(false), token, + window.control(), actions, () -> new IndexView(TITLE, wallet.start()) )); final URI uri = server.start(); @@ -83,21 +135,31 @@ private static void start(final LaunchOptions options) throws Exception { try { window.open(uri.resolve("/?" + SessionToken.PARAMETER + "=" + token.value())); } catch (Exception | LinkageError e) { - giveUp(server, e); + giveUp(application, server, e); } } /* The server's threads would keep the process alive with no window to reach it, and a launcher from a menu has no console to show the error in. */ - private static void giveUp(final UiServer server, final Throwable cause) throws Exception { + private static void giveUp(final Application application, final UiServer server, final Throwable cause) + throws Exception { + LOG.error("Janus could not open its window", cause); server.stop(); if (!GraphicsEnvironment.isHeadless()) { - JOptionPane.showMessageDialog(null, StartFailure.message(new CefLocation(), cause), TITLE, - JOptionPane.ERROR_MESSAGE); + final String message = EngineFailure.message(application, cause); + + JOptionPane.showMessageDialog(null, message, TITLE, JOptionPane.ERROR_MESSAGE); } System.exit(1); } + + /** What Janus itself takes on the command line, besides the window system Oriel takes. */ + @Command(name = "Unigrid", description = "The Unigrid Control Center.") + private static final class Arguments { + @Option(names = { "-h", "--help" }, usageHelp = true, description = "Show this help and exit.") + private boolean help; + } } diff --git a/shell/src/main/java/org/unigrid/janus/shell/LaunchOptions.java b/shell/src/main/java/org/unigrid/janus/shell/LaunchOptions.java deleted file mode 100644 index c2796430..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/LaunchOptions.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.util.Optional; -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Model.OptionSpec; -import picocli.CommandLine.Option; - -@Command(name = "Unigrid", description = "The Unigrid Control Center.") -public final class LaunchOptions { - private static final String WINDOW_SYSTEM = "--window-system"; - - @Option(names = WINDOW_SYSTEM, paramLabel = "", defaultValue = "x11", - description = "What the page is drawn on in a Linux desktop session: x11, wayland or auto " - + "(defaults to ${DEFAULT-VALUE}, which embeds it in the window on X11 and Wayland desktops alike)." - ) - private WindowSystem windowSystem; - - @Option(names = { "-h", "--help" }, usageHelp = true, description = "Show this help and exit.") - private boolean help; - - private LaunchOptions() { - } - - /** Empty when the help was asked for, which has then been printed instead. */ - public static Optional of(final String... args) { - return of(WindowSystem.CHOOSABLE, args); - } - - /* Where there is no window system to choose, the option is still taken but kept out of the help. */ - static Optional of(final boolean windowSystemChoosable, final String... args) { - final LaunchOptions options = new LaunchOptions(); - final CommandLine commandLine = new CommandLine(options).setCaseInsensitiveEnumValuesAllowed(true); - - if (!windowSystemChoosable) { - final CommandSpec spec = commandLine.getCommandSpec(); - final OptionSpec windowSystem = spec.findOption(WINDOW_SYSTEM); - - spec.remove(windowSystem); - spec.addOption(OptionSpec.builder(windowSystem).hidden(true).build()); - } - - return CommandLine.printHelpIfRequested(commandLine.parseArgs(args)) ? Optional.empty() - : Optional.of(options); - } - - public WindowSystem windowSystem() { - return windowSystem; - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/ResizeBounds.java b/shell/src/main/java/org/unigrid/janus/shell/ResizeBounds.java deleted file mode 100644 index 950825cb..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/ResizeBounds.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Dimension; -import java.awt.Point; -import java.awt.Rectangle; -import org.unigrid.janus.web.WindowControl.Edge; - -/** Where a window ends up once one of its edges has travelled with the pointer. */ -public final class ResizeBounds { - private ResizeBounds() { - } - - public static Rectangle resized(final Edge edge, final Rectangle origin, final Point travel, - final Dimension minimum) { - - final int wider = Math.max(minimum.width, origin.width + travel.x); - final int taller = Math.max(minimum.height, origin.height + travel.y); - - return switch (edge) { - case LEFT -> { - final int width = Math.max(minimum.width, origin.width - travel.x); - - yield new Rectangle(origin.x + origin.width - width, origin.y, width, origin.height); - } - case RIGHT -> new Rectangle(origin.x, origin.y, wider, origin.height); - case BOTTOM -> new Rectangle(origin.x, origin.y, origin.width, taller); - case BOTTOM_RIGHT -> new Rectangle(origin.x, origin.y, wider, taller); - }; - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/ShutdownDeadline.java b/shell/src/main/java/org/unigrid/janus/shell/ShutdownDeadline.java deleted file mode 100644 index b67afb41..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/ShutdownDeadline.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.time.Duration; - -/** Ends the process once a shutdown has had its time, for the times it never finishes on its own. */ -final class ShutdownDeadline { - private ShutdownDeadline() { - } - - /* A daemon, so a shutdown that does finish in time is never held up by the wait. */ - static void start(final Duration grace, final Runnable end) { - Thread.ofPlatform().name("shutdown-deadline").daemon().start(() -> { - try { - Thread.sleep(grace); - end.run(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/StartFailure.java b/shell/src/main/java/org/unigrid/janus/shell/StartFailure.java deleted file mode 100644 index c5c089d9..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/StartFailure.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.util.Objects; - -/** - * What a person sees when the window cannot be opened. A launched application has no console, so without - * this a damaged browser engine would leave a server running and nothing on the screen. - */ -final class StartFailure { - private StartFailure() { - } - - static String message(final CefLocation location, final Throwable cause) { - final String reason = Objects.requireNonNullElse(cause.getMessage(), cause.getClass().getSimpleName()); - - return "Janus could not start its browser engine from " + location.directory() + ".\n\n" + reason - + "\n\nInstall Janus again to restore it."; - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/WindowIcon.java b/shell/src/main/java/org/unigrid/janus/shell/WindowIcon.java deleted file mode 100644 index a5de7f1a..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/WindowIcon.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Image; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.List; -import java.util.stream.IntStream; -import javax.imageio.ImageIO; - -/** - * The Unigrid logo at the sizes window managers and task bars ask for. Each size is drawn - * ahead of time because scaling the large logo down at run time leaves the small ones blurred. - */ -public final class WindowIcon { - private static final int[] SIZES = {16, 24, 32, 48, 64, 128, 256}; - - private WindowIcon() { - } - - public static List images() { - return IntStream.of(SIZES).mapToObj(WindowIcon::read).toList(); - } - - private static Image read(final int size) { - try { - return ImageIO.read(WindowIcon.class.getResource("icons/unigrid-%d.png".formatted(size))); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/WindowShape.java b/shell/src/main/java/org/unigrid/janus/shell/WindowShape.java deleted file mode 100644 index ec4b93e8..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/WindowShape.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Dimension; -import java.awt.Frame; -import java.awt.Shape; -import java.awt.geom.RoundRectangle2D; -import java.util.Optional; - -/** - * The outline of the main window: rounded corners while it floats and square corners while it is - * maximised, the way native windows behave. - */ -public final class WindowShape { - private static final int RADIUS = 12; - private static final int ARC = RADIUS * 2; - - private WindowShape() { - } - - public static Optional of(final int extendedState, final Dimension size) { - if ((extendedState & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) { - return Optional.empty(); - } - - return Optional.of(new RoundRectangle2D.Double(0, 0, size.width, size.height, ARC, ARC)); - } -} diff --git a/shell/src/main/java/org/unigrid/janus/shell/WindowSystem.java b/shell/src/main/java/org/unigrid/janus/shell/WindowSystem.java deleted file mode 100644 index a2a8585c..00000000 --- a/shell/src/main/java/org/unigrid/janus/shell/WindowSystem.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -/** - * What the browser engine draws the page on in a Linux desktop session. The frame around it is an X11 window - * even on a Wayland desktop, drawn through Xwayland, and the engine can only be embedded in it from X11. Left - * to choose, it runs on Wayland there and draws the page in a window of its own, and on GNOME it loads GTK 4, - * which deadlocks the GTK 3 the file picker needs. - */ -public enum WindowSystem { - X11("--ozone-platform=x11"), - WAYLAND("--ozone-platform=wayland"), - AUTO("--ozone-platform-hint=auto"); - - /** Only a Linux desktop session leaves the engine a window system to choose. */ - static final boolean CHOOSABLE = System.getProperty("os.name").startsWith("Linux"); - - private final String engineSwitch; - - WindowSystem(final String engineSwitch) { - this.engineSwitch = engineSwitch; - } - - public String engineSwitch() { - return engineSwitch; - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/CefLocationTest.java b/shell/src/test/java/org/unigrid/janus/shell/CefLocationTest.java deleted file mode 100644 index c2677ed8..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/CefLocationTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.io.File; -import java.nio.file.Path; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class CefLocationTest { - private static final Path HOME = Path.of("/home/someone"); - - @Test - public void shouldKeepTheEngineInTheHomeFolderWhenNoneIsBundled() { - final CefLocation location = new CefLocation(null, HOME); - - assertFalse(location.isBundled()); - assertEquals(HOME.resolve(".janus/jcef").toFile(), location.directory()); - } - - @Test - public void shouldUseTheFolderTheLauncherNames() { - final CefLocation location = new CefLocation("/opt/unigrid/lib/app/jcef", HOME); - - assertTrue(location.isBundled()); - assertEquals(new File("/opt/unigrid/lib/app/jcef"), location.directory()); - } - - @Test - public void shouldIgnoreAnEmptyOrBlankProperty() { - for (final String value : new String[] {"", " "}) { - final CefLocation location = new CefLocation(value, HOME); - - assertFalse(location.isBundled()); - assertEquals(HOME.resolve(".janus/jcef").toFile(), location.directory()); - } - } - - @Test - public void shouldTrustTheBundledFolderEvenWhenItIsMissing() { - final CefLocation location = new CefLocation("/nowhere/jcef", HOME); - - assertTrue(location.isBundled()); - assertFalse(location.directory().exists()); - } - - @Test - public void shouldPreferTheBundledEngineOverOneInTheHomeFolder() { - final Path home = Path.of(System.getProperty("java.io.tmpdir")); - final CefLocation location = new CefLocation("/opt/unigrid/lib/app/jcef", home); - - assertEquals(new File("/opt/unigrid/lib/app/jcef"), location.directory()); - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/EngineFailureTest.java b/shell/src/test/java/org/unigrid/janus/shell/EngineFailureTest.java new file mode 100644 index 00000000..510b9d35 --- /dev/null +++ b/shell/src/test/java/org/unigrid/janus/shell/EngineFailureTest.java @@ -0,0 +1,71 @@ +/* + The Janus Wallet + Copyright © 2021-2026 Stiftelsen The Unigrid Foundation + + This program is free software: you can redistribute it and/or modify it under the terms of the + addended GNU Affero General Public License as published by the Free Software Foundation, version 3 + of the License (see COPYING and COPYING.addendum). + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received an addended copy of the GNU Affero General Public License with this program. + If not, see and . + */ + +package org.unigrid.janus.shell; + +import java.awt.Dimension; +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.unigrid.oriel.server.AppId; +import org.unigrid.oriel.window.Application; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EngineFailureTest { + /* The property the installer's launcher names the bundled engine with, as desktop/pom.xml passes it. */ + private static final String BUNDLED = "janus.jcef"; + private static final File INSTALLED = new File("/opt/unigrid/lib/app/jcef"); + private static final Application UNIGRID = new Application(AppId.of("janus"), "Unigrid", List.of(), + new Dimension(1240, 800), new Dimension(900, 640) + ); + + private String kept; + + @BeforeEach + public void forgetTheBundledEngine() { + kept = System.getProperty(BUNDLED); + System.clearProperty(BUNDLED); + } + + @AfterEach + public void restoreTheBundledEngine() { + if (kept == null) { + System.clearProperty(BUNDLED); + } else { + System.setProperty(BUNDLED, kept); + } + } + + @Test + public void shouldNameTheHomeFolderWhenNoEngineIsBundled() { + final Path home = Path.of(System.getProperty("user.home"), ".janus", "jcef"); + final String message = EngineFailure.message(UNIGRID, new IllegalStateException("x")); + + assertTrue(message.contains(home.toString()), message); + } + + @Test + public void shouldNameTheInstalledEngineWhenOneIsBundled() { + System.setProperty(BUNDLED, INSTALLED.getPath()); + + final String message = EngineFailure.message(UNIGRID, new UnsatisfiedLinkError("libcef.so")); + + assertTrue(message.contains(INSTALLED.toString()), message); + } +} diff --git a/shell/src/test/java/org/unigrid/janus/shell/JanusTest.java b/shell/src/test/java/org/unigrid/janus/shell/JanusTest.java new file mode 100644 index 00000000..80a64351 --- /dev/null +++ b/shell/src/test/java/org/unigrid/janus/shell/JanusTest.java @@ -0,0 +1,74 @@ +/* + The Janus Wallet + Copyright © 2021-2026 Stiftelsen The Unigrid Foundation + + This program is free software: you can redistribute it and/or modify it under the terms of the + addended GNU Affero General Public License as published by the Free Software Foundation, version 3 + of the License (see COPYING and COPYING.addendum). + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received an addended copy of the GNU Affero General Public License with this program. + If not, see and . + */ + +package org.unigrid.janus.shell; + +import java.io.PrintWriter; +import java.io.StringWriter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JanusTest { + /* What a log line starts with, so that plain output can be told from what went through the logger. */ + private static final String LOG_PREFIX = "^\\d{2}:\\d{2}:\\d{2}\\.\\d{3} .*"; + + private final StringWriter out = new StringWriter(); + private final StringWriter err = new StringWriter(); + private CommandLine command; + + /* picocli colours the help where it takes the console to understand colours, as on the Windows runners, which + puts escape codes inside the text, so the tests read it without them. */ + @BeforeEach + public void captureWhatJanusPrints() { + command = Janus.command(); + command.setColorScheme(CommandLine.Help.defaultColorScheme(CommandLine.Help.Ansi.OFF)); + command.setOut(new PrintWriter(out)); + command.setErr(new PrintWriter(err)); + } + + @Test + public void shouldPrintTheHelpPlainlyAndNotStart() { + assertTrue(Janus.options(command, "--help").isEmpty(), "Janus would start after showing the help"); + assertTrue(out.toString().startsWith("Usage: Unigrid"), out.toString()); + assertFalse(out.toString().lines().anyMatch(line -> line.matches(LOG_PREFIX)), out.toString()); + } + + @Test + public void shouldRefuseAnOptionItDoesNotTakeAndSayWhyPlainly() { + final CommandLine.ParameterException refused = assertThrows(CommandLine.ParameterException.class, + () -> Janus.options(command, "--bogus") + ); + + Janus.refuse(command, refused); + assertTrue(err.toString().startsWith(refused.getMessage()), err.toString()); + assertTrue(err.toString().contains("--bogus"), err.toString()); + assertTrue(err.toString().contains("Usage: Unigrid"), err.toString()); + } + + /* Oriel reads the window system on every desktop, even where it hides it from the help, so this holds anywhere. */ + @Test + public void shouldRefuseAWindowSystemOrielDoesNotKnow() { + final IllegalArgumentException refused = assertThrows(IllegalArgumentException.class, + () -> Janus.options(command, "--window-system=bogus") + ); + + assertTrue(refused.getMessage().contains("bogus"), refused.getMessage()); + } +} diff --git a/shell/src/test/java/org/unigrid/janus/shell/LaunchOptionsTest.java b/shell/src/test/java/org/unigrid/janus/shell/LaunchOptionsTest.java deleted file mode 100644 index 9fc0645e..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/LaunchOptionsTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.Test; -import picocli.CommandLine; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class LaunchOptionsTest { - private static final String WINDOW_SYSTEM = "--window-system"; - - @Test - public void shouldDrawOnX11WhenNothingIsSaid() { - assertEquals(WindowSystem.X11, LaunchOptions.of().orElseThrow().windowSystem()); - } - - @Test - public void shouldTakeTheWindowSystemInAnyCase() { - assertEquals(WindowSystem.WAYLAND, LaunchOptions.of("--window-system=wayland").orElseThrow().windowSystem()); - assertEquals(WindowSystem.AUTO, LaunchOptions.of("--window-system", "Auto").orElseThrow().windowSystem()); - } - - @Test - public void shouldRefuseAWindowSystemItDoesNotKnow() { - assertThrows(CommandLine.ParameterException.class, () -> LaunchOptions.of("--window-system=mir")); - } - - @Test - public void shouldRefuseAnOptionItDoesNotKnow() { - assertThrows(CommandLine.ParameterException.class, () -> LaunchOptions.of("--frobnicate")); - } - - @Test - public void shouldStartNothingWhenAskedForHelp() { - assertTrue(LaunchOptions.of("--help").isEmpty()); - } - - @Test - public void shouldOfferTheWindowSystemWhereItCanBeChosen() { - final String help = help(true); - - assertTrue(help.contains(WINDOW_SYSTEM), help); - } - - @Test - public void shouldKeepTheWindowSystemOutOfTheHelpWhereItCannotBeChosen() { - final String help = help(false); - - assertFalse(help.contains(WINDOW_SYSTEM), help); - assertEquals(WindowSystem.WAYLAND, LaunchOptions.of(false, WINDOW_SYSTEM + "=wayland").orElseThrow() - .windowSystem()); - } - - private static String help(final boolean windowSystemChoosable) { - final PrintStream stdout = System.out; - final ByteArrayOutputStream printed = new ByteArrayOutputStream(); - - System.setOut(new PrintStream(printed, true, StandardCharsets.UTF_8)); - - try { - LaunchOptions.of(windowSystemChoosable, "--help"); - } finally { - System.setOut(stdout); - } - - return printed.toString(StandardCharsets.UTF_8); - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/OpeningBoundsTest.java b/shell/src/test/java/org/unigrid/janus/shell/OpeningBoundsTest.java deleted file mode 100644 index c3e4fcf2..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/OpeningBoundsTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Dimension; -import java.awt.Rectangle; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class OpeningBoundsTest { - private static final Dimension PREFERRED = new Dimension(1240, 800); - - @Test - public void shouldOpenAtThePreferredSizeInTheMiddleOfARoomyScreen() { - assertEquals( - new Rectangle(340, 120, 1240, 800), - OpeningBounds.centred(PREFERRED, new Rectangle(0, 0, 1920, 1040)) - ); - } - - @Test - public void shouldShrinkToTheHeightOfAShortScreen() { - assertEquals( - new Rectangle(63, 0, 1240, 728), - OpeningBounds.centred(PREFERRED, new Rectangle(0, 0, 1366, 728)) - ); - } - - @Test - public void shouldShrinkToTheWidthOfANarrowScreen() { - assertEquals( - new Rectangle(0, 20, 1024, 800), - OpeningBounds.centred(PREFERRED, new Rectangle(0, 0, 1024, 840)) - ); - } - - @Test - public void shouldStayClearOfATaskbarAtTheTopOrLeft() { - assertEquals( - new Rectangle(48, 32, 1232, 688), - OpeningBounds.centred(PREFERRED, new Rectangle(48, 32, 1232, 688)) - ); - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/ResizeBoundsTest.java b/shell/src/test/java/org/unigrid/janus/shell/ResizeBoundsTest.java deleted file mode 100644 index bb19ebdc..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/ResizeBoundsTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Dimension; -import java.awt.Point; -import java.awt.Rectangle; -import org.junit.jupiter.api.Test; -import org.unigrid.janus.web.WindowControl.Edge; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class ResizeBoundsTest { - private static final Rectangle ORIGIN = new Rectangle(100, 100, 1000, 700); - private static final Dimension MINIMUM = new Dimension(900, 640); - - @Test - public void shouldGrowToTheRightWhenTheRightEdgeIsPulledOut() { - assertEquals( - new Rectangle(100, 100, 1050, 700), - ResizeBounds.resized(Edge.RIGHT, ORIGIN, new Point(50, 999), MINIMUM) - ); - } - - @Test - public void shouldKeepTheRightEdgeStillWhenTheLeftEdgeIsPulledOut() { - assertEquals( - new Rectangle(60, 100, 1040, 700), - ResizeBounds.resized(Edge.LEFT, ORIGIN, new Point(-40, 999), MINIMUM) - ); - } - - @Test - public void shouldGrowDownwardsWhenTheBottomEdgeIsPulledOut() { - assertEquals( - new Rectangle(100, 100, 1000, 730), - ResizeBounds.resized(Edge.BOTTOM, ORIGIN, new Point(999, 30), MINIMUM) - ); - } - - @Test - public void shouldGrowBothWaysFromTheCorner() { - assertEquals( - new Rectangle(100, 100, 1050, 730), - ResizeBounds.resized(Edge.BOTTOM_RIGHT, ORIGIN, new Point(50, 30), MINIMUM) - ); - } - - @Test - public void shouldStopAtTheMinimumWithTheRightEdgeStillWhenPushingTheLeftEdgeIn() { - assertEquals( - new Rectangle(200, 100, 900, 700), - ResizeBounds.resized(Edge.LEFT, ORIGIN, new Point(500, 0), MINIMUM) - ); - } - - @Test - public void shouldStopAtTheMinimumWhenPushingTheCornerIn() { - assertEquals( - new Rectangle(100, 100, 900, 640), - ResizeBounds.resized(Edge.BOTTOM_RIGHT, ORIGIN, new Point(-500, -500), MINIMUM) - ); - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/ShutdownDeadlineTest.java b/shell/src/test/java/org/unigrid/janus/shell/ShutdownDeadlineTest.java deleted file mode 100644 index dba2302f..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/ShutdownDeadlineTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ShutdownDeadlineTest { - private static final long WAIT_SECONDS = 5; - - @Test - public void shouldEndTheProcessOnceTheGraceHasPassed() throws Exception { - final CompletableFuture ended = new CompletableFuture<>(); - - ShutdownDeadline.start(Duration.ofMillis(50), () -> ended.complete(Thread.currentThread().isDaemon())); - assertTrue(ended.get(WAIT_SECONDS, TimeUnit.SECONDS), "The deadline would keep the process alive"); - } - - @Test - public void shouldWaitOutTheGraceFirst() throws Exception { - final CompletableFuture ended = new CompletableFuture<>(); - - ShutdownDeadline.start(Duration.ofSeconds(WAIT_SECONDS), () -> ended.complete(true)); - Thread.sleep(100); - assertFalse(ended.isDone()); - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/StartFailureTest.java b/shell/src/test/java/org/unigrid/janus/shell/StartFailureTest.java deleted file mode 100644 index 17b004c8..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/StartFailureTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.nio.file.Path; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class StartFailureTest { - private static final Path HOME = Path.of("/home/someone"); - - @Test - public void shouldNameTheFolderTheEngineWasLoadedFrom() { - final CefLocation location = new CefLocation("/opt/unigrid/lib/app/jcef", HOME); - final String message = StartFailure.message(location, - new UnsatisfiedLinkError("libcef.so: cannot open shared object file")); - - assertTrue(message.contains(location.directory().toString()), message); - } - - @Test - public void shouldSayWhatWentWrong() { - final String message = StartFailure.message(new CefLocation(null, HOME), - new UnsatisfiedLinkError("libcef.so: cannot open shared object file")); - - assertTrue(message.contains("libcef.so: cannot open shared object file"), message); - } - - @Test - public void shouldNameTheHomeFolderWhenNoEngineIsBundled() { - final String message = StartFailure.message(new CefLocation(null, HOME), new IllegalStateException("x")); - - assertTrue(message.contains(HOME.resolve(".janus/jcef").toString()), message); - } - - @Test - public void shouldStillSpeakWhenTheFailureHasNoMessage() { - final String message = StartFailure.message(new CefLocation(null, HOME), new IllegalStateException()); - - assertTrue(message.contains("IllegalStateException"), message); - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/WindowIconTest.java b/shell/src/test/java/org/unigrid/janus/shell/WindowIconTest.java deleted file mode 100644 index 8aca1463..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/WindowIconTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Image; -import java.util.List; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class WindowIconTest { - @Test - public void shouldProvideTheLogoAtEveryStandardIconSize() { - assertEquals( - List.of(16, 24, 32, 48, 64, 128, 256), - WindowIcon.images().stream().map(image -> image.getWidth(null)).toList() - ); - } - - @Test - public void shouldDrawEverySizeSquare() { - for (Image image : WindowIcon.images()) { - assertEquals(image.getWidth(null), image.getHeight(null)); - } - } -} diff --git a/shell/src/test/java/org/unigrid/janus/shell/WindowShapeTest.java b/shell/src/test/java/org/unigrid/janus/shell/WindowShapeTest.java deleted file mode 100644 index b6b27150..00000000 --- a/shell/src/test/java/org/unigrid/janus/shell/WindowShapeTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.shell; - -import java.awt.Dimension; -import java.awt.Frame; -import java.awt.geom.RoundRectangle2D; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class WindowShapeTest { - private static final Dimension SIZE = new Dimension(1100, 720); - - @Test - public void shouldRoundTheCornersOfANormalWindowByTwelvePixels() { - assertEquals( - new RoundRectangle2D.Double(0, 0, 1100, 720, 24, 24), - WindowShape.of(Frame.NORMAL, SIZE).orElseThrow() - ); - } - - @Test - public void shouldKeepTheCornersOfAMaximisedWindowSquare() { - assertTrue(WindowShape.of(Frame.MAXIMIZED_BOTH, SIZE).isEmpty()); - } - - @Test - public void shouldStaySquareWhenMaximisedTogetherWithOtherStates() { - assertTrue(WindowShape.of(Frame.MAXIMIZED_BOTH | Frame.ICONIFIED, SIZE).isEmpty()); - } -} diff --git a/ui/pom.xml b/ui/pom.xml index b7b8cfd9..24212292 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -17,9 +17,8 @@ ${project.version} - org.unigrid.janus - web - ${project.version} + ${oriel.groupId} + oriel-server org.projectlombok @@ -43,9 +42,8 @@ archunit - org.unigrid.janus - web - ${project.version} + ${oriel.groupId} + oriel-server test-jar test diff --git a/ui/src/main/java/org/unigrid/janus/ui/controller/AboutController.java b/ui/src/main/java/org/unigrid/janus/ui/controller/AboutController.java index 79703ff2..786812b1 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/controller/AboutController.java +++ b/ui/src/main/java/org/unigrid/janus/ui/controller/AboutController.java @@ -20,7 +20,7 @@ of the License (see COPYING and COPYING.addendum). import jakarta.inject.Inject; import org.unigrid.janus.core.Release; import org.unigrid.janus.ui.view.AboutView; -import org.unigrid.janus.web.action.Action; +import org.unigrid.oriel.server.action.Action; @ApplicationScoped public class AboutController { diff --git a/ui/src/main/java/org/unigrid/janus/ui/controller/ImportController.java b/ui/src/main/java/org/unigrid/janus/ui/controller/ImportController.java index ed3a6979..d276d84b 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/controller/ImportController.java +++ b/ui/src/main/java/org/unigrid/janus/ui/controller/ImportController.java @@ -32,8 +32,8 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.core.legacy.UnreadableWallet; import org.unigrid.janus.core.legacy.WalletDump; import org.unigrid.janus.ui.view.ImportView; -import org.unigrid.janus.web.action.Action; -import org.unigrid.janus.web.action.Form; +import org.unigrid.oriel.server.action.Action; +import org.unigrid.oriel.server.action.Form; @ApplicationScoped public class ImportController { diff --git a/ui/src/main/java/org/unigrid/janus/ui/controller/PhraseController.java b/ui/src/main/java/org/unigrid/janus/ui/controller/PhraseController.java index 1399ab2c..8e5a3593 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/controller/PhraseController.java +++ b/ui/src/main/java/org/unigrid/janus/ui/controller/PhraseController.java @@ -38,9 +38,9 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.ui.view.SeedView; import org.unigrid.janus.ui.view.VerifyView; import org.unigrid.janus.ui.view.WelcomeView; -import org.unigrid.janus.web.action.Action; -import org.unigrid.janus.web.action.Form; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.Action; +import org.unigrid.oriel.server.action.Form; +import org.unigrid.oriel.server.action.View; /** * Making an EVM wallet from a new recovery phrase, or from one the user already has. The phrase lives in diff --git a/ui/src/main/java/org/unigrid/janus/ui/controller/WalletController.java b/ui/src/main/java/org/unigrid/janus/ui/controller/WalletController.java index b9c23de4..f38fd570 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/controller/WalletController.java +++ b/ui/src/main/java/org/unigrid/janus/ui/controller/WalletController.java @@ -42,30 +42,30 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.core.wallet.WalletCsv; import org.unigrid.janus.core.wallet.WalletFunds; import org.unigrid.janus.core.wallet.WalletLedger; -import org.unigrid.janus.core.wallet.WalletTransaction; import org.unigrid.janus.core.wallet.WalletTransaction.Kind; -import org.unigrid.janus.ui.view.ActivityView; +import org.unigrid.janus.core.wallet.WalletTransaction; import org.unigrid.janus.ui.view.ActivityView.Summary; +import org.unigrid.janus.ui.view.ActivityView; import org.unigrid.janus.ui.view.Amounts; -import org.unigrid.janus.ui.view.AppView; import org.unigrid.janus.ui.view.AppView.Tab; -import org.unigrid.janus.ui.view.DashboardView; +import org.unigrid.janus.ui.view.AppView; import org.unigrid.janus.ui.view.DashboardView.Bar; import org.unigrid.janus.ui.view.DashboardView.FundedPage; import org.unigrid.janus.ui.view.DashboardView.Holding; +import org.unigrid.janus.ui.view.DashboardView; import org.unigrid.janus.ui.view.ExportNoteView; import org.unigrid.janus.ui.view.Filter; import org.unigrid.janus.ui.view.FundedBodyView; import org.unigrid.janus.ui.view.ImportView; -import org.unigrid.janus.ui.view.PreparingView; -import org.unigrid.janus.ui.view.PreparingView.Step; import org.unigrid.janus.ui.view.PreparingView.Step.State; +import org.unigrid.janus.ui.view.PreparingView.Step; +import org.unigrid.janus.ui.view.PreparingView; import org.unigrid.janus.ui.view.RowsView; import org.unigrid.janus.ui.view.Times; import org.unigrid.janus.ui.view.WelcomeView; -import org.unigrid.janus.web.action.Action; -import org.unigrid.janus.web.action.Form; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.Action; +import org.unigrid.oriel.server.action.Form; +import org.unigrid.oriel.server.action.View; /** * The wallet once it is chosen: getting its ledger ready and showing it. Janus has a single window, so diff --git a/ui/src/main/java/org/unigrid/janus/ui/controller/WelcomeController.java b/ui/src/main/java/org/unigrid/janus/ui/controller/WelcomeController.java index d0cf54b0..2e7e870d 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/controller/WelcomeController.java +++ b/ui/src/main/java/org/unigrid/janus/ui/controller/WelcomeController.java @@ -18,7 +18,7 @@ of the License (see COPYING and COPYING.addendum). import jakarta.enterprise.context.ApplicationScoped; import org.unigrid.janus.ui.view.WelcomeView; -import org.unigrid.janus.web.action.Action; +import org.unigrid.oriel.server.action.Action; @ApplicationScoped public class WelcomeController { diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/AboutView.java b/ui/src/main/java/org/unigrid/janus/ui/view/AboutView.java index 6f5a812c..df523dbd 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/AboutView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/AboutView.java @@ -16,7 +16,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; public record AboutView(String version) implements View { @Override diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/ActivityView.java b/ui/src/main/java/org/unigrid/janus/ui/view/ActivityView.java index 81b3de0b..c11b4b30 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/ActivityView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/ActivityView.java @@ -17,7 +17,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; import java.util.List; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** Every transaction of the wallet, narrowed by kind and by what was searched for. */ public record ActivityView(Filter filter, String query, List summaries, RowsView rows, ExportNoteView note) diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/AppView.java b/ui/src/main/java/org/unigrid/janus/ui/view/AppView.java index 2aff9d22..28b5b5ee 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/AppView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/AppView.java @@ -16,7 +16,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** * The wallet's own window: its tabs, the state of the ledger and the screen being shown. It is drawn and diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/DashboardView.java b/ui/src/main/java/org/unigrid/janus/ui/view/DashboardView.java index 03e38301..4d443adc 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/DashboardView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/DashboardView.java @@ -20,7 +20,7 @@ of the License (see COPYING and COPYING.addendum). import java.math.RoundingMode; import java.util.List; import java.util.Locale; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** * The wallet at a glance: what it holds, how that grew, how its addresses stand and what happened last. diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/ExportNoteView.java b/ui/src/main/java/org/unigrid/janus/ui/view/ExportNoteView.java index 63ff3ca7..7a8029bc 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/ExportNoteView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/ExportNoteView.java @@ -16,7 +16,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** What became of the last export, shown next to the button that made it. */ public record ExportNoteView(String message, boolean failed) implements View { diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/FundedBodyView.java b/ui/src/main/java/org/unigrid/janus/ui/view/FundedBodyView.java index 994bb4e5..66372d57 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/FundedBodyView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/FundedBodyView.java @@ -17,7 +17,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; import org.unigrid.janus.ui.view.DashboardView.FundedPage; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** * The list of funded addresses and its pager without the dialog around them, so that a page or a filter diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/ImportView.java b/ui/src/main/java/org/unigrid/janus/ui/view/ImportView.java index 63d0cf62..ef582d92 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/ImportView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/ImportView.java @@ -17,7 +17,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; import java.nio.file.Path; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** * The import step: where a wallet was looked for, the one found there, the one settled on, where that one diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/IndexView.java b/ui/src/main/java/org/unigrid/janus/ui/view/IndexView.java index 384f8f9d..3728fb84 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/IndexView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/IndexView.java @@ -16,7 +16,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** The page the window opens on, and the view inside it: the welcome card, or the wallet once one is chosen. */ public record IndexView(String title, View content) implements View { diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/PasswordView.java b/ui/src/main/java/org/unigrid/janus/ui/view/PasswordView.java index 4c6fa1bf..c806e31a 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/PasswordView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/PasswordView.java @@ -16,7 +16,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** * The password the recovery phrase is sealed with before it is kept, whether the keys of an imported wallet diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/PreparingView.java b/ui/src/main/java/org/unigrid/janus/ui/view/PreparingView.java index 91b84141..074be47a 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/PreparingView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/PreparingView.java @@ -18,7 +18,7 @@ of the License (see COPYING and COPYING.addendum). import java.util.List; import java.util.Locale; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** How far the wallet is from being shown, and what went wrong when it cannot be. */ public record PreparingView(List steps, String failure, boolean chooseAnother) implements View { diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/RestoreView.java b/ui/src/main/java/org/unigrid/janus/ui/view/RestoreView.java index d630108f..ffdfd31d 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/RestoreView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/RestoreView.java @@ -17,7 +17,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; import java.util.List; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** Twelve boxes for a recovery phrase, holding what was typed and why it was refused, if it was. */ public record RestoreView(List words, String error) implements View { diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/RowsView.java b/ui/src/main/java/org/unigrid/janus/ui/view/RowsView.java index 2fdebb0b..879f759d 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/RowsView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/RowsView.java @@ -17,7 +17,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; import java.util.List; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** * A page of the ledger. The first page is the list itself; the pages after it take the place of the diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/SeedView.java b/ui/src/main/java/org/unigrid/janus/ui/view/SeedView.java index ec3c16e8..558ace5d 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/SeedView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/SeedView.java @@ -17,7 +17,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; import java.util.List; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** The recovery phrase of a wallet being made, shown once so it can be written down. */ public record SeedView(List words) implements View { diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/VerifyView.java b/ui/src/main/java/org/unigrid/janus/ui/view/VerifyView.java index 999d4fe9..e0c53a43 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/VerifyView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/VerifyView.java @@ -17,7 +17,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; import java.util.List; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; /** * Proof that the phrase was written down: its words, shuffled into tiles, tapped back in order. A tile diff --git a/ui/src/main/java/org/unigrid/janus/ui/view/WelcomeView.java b/ui/src/main/java/org/unigrid/janus/ui/view/WelcomeView.java index bf1cd40f..9454dbcb 100644 --- a/ui/src/main/java/org/unigrid/janus/ui/view/WelcomeView.java +++ b/ui/src/main/java/org/unigrid/janus/ui/view/WelcomeView.java @@ -16,7 +16,7 @@ of the License (see COPYING and COPYING.addendum). package org.unigrid.janus.ui.view; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; public record WelcomeView() implements View { @Override diff --git a/ui/src/main/resources/static/css/janus.css b/ui/src/main/resources/static/css/janus.css index 60d99c9f..1dfeb9b5 100644 --- a/ui/src/main/resources/static/css/janus.css +++ b/ui/src/main/resources/static/css/janus.css @@ -541,7 +541,8 @@ main { align-items: center; } -.step__error { +.step__error, +[data-failure] { margin: 14px 0 0; font-size: 13px; color: var(--danger); diff --git a/ui/src/main/resources/static/js/failure.js b/ui/src/main/resources/static/js/failure.js deleted file mode 100644 index 602bc0d2..00000000 --- a/ui/src/main/resources/static/js/failure.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -/* A failed action leaves the page as it was, which makes a click that failed look like one that was ignored. - The reason is written to the log, so the person is only told that there is one. */ -document.addEventListener("htmx:responseError", () => { - const host = document.querySelector("main > .card") ?? document.querySelector("main"); - const notice = host.querySelector("[data-failure]") ?? host.appendChild(document.createElement("p")); - - notice.className = "step__error"; - notice.dataset.failure = ""; - notice.setAttribute("role", "alert"); - notice.textContent = "That did not work. The reason is in the log in the logs folder inside .janus in your home folder."; -}); diff --git a/ui/src/main/resources/static/js/htmx.min.js b/ui/src/main/resources/static/js/htmx.min.js deleted file mode 100644 index 3b7ac1ac..00000000 --- a/ui/src/main/resources/static/js/htmx.min.js +++ /dev/null @@ -1 +0,0 @@ -var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true,reportValidityOfForms:false},parseInterval:null,location:location,_:null,version:"2.0.10"};Q.onLoad=j;Q.process=Ft;Q.on=ye;Q.off=xe;Q.trigger=ae;Q.ajax=Nn;Q.find=f;Q.findAll=y;Q.closest=g;Q.remove=z;Q.addClass=w;Q.removeClass=b;Q.toggleClass=G;Q.takeClass=W;Q.swap=_e;Q.defineExtension=_n;Q.removeExtension=zn;Q.logAll=$;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:U,findThisElement:we,filterValues:yn,swap:_e,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:A,getExpressionVars:Rn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Se,makeFragment:P,mergeObjects:le,makeSettleInfo:Sn,oobSwap:He,querySelectorExt:ce,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:Vt};const de=["get","post","put","delete","patch"];const R=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function q(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function A(e,t){while(e&&!t(e)){e=c(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;A(t,function(e){return!!(r=o(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function N(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function I(e){if("parseHTMLUnsafe"in Document){return Document.parseHTMLUnsafe(e)}const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function D(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){H(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=N(t);let r;if(n==="html"){r=new DocumentFragment;const i=I(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=I(t);L(r,i.body);r.title=i.title}else{const i=I('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){D(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function M(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function F(t){const n=[];if(t){for(let e=0;e=0}function se(e){return e.getRootNode({composed:true})===document}function X(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){H(e);return null}}function U(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function V(e){try{const t=new URL(e,window.location.href);e=t.pathname+t.search}catch(e){}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function $(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function y(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return y(te(),e)}}function x(){return window}function z(e,t){e=S(e);if(t){x().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function J(e){return e instanceof HTMLElement?e:null}function K(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function w(e,t,n){e=ue(S(e));if(!e){return}if(n){x().setTimeout(function(){w(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function b(e,t,n){let r=ue(S(e));if(!r){return}if(n){x().setTimeout(function(){b(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function G(e,t){e=S(e);e.classList.toggle(t)}function W(e,t){e=S(e);ie(e.parentElement.children,function(e){b(e,t)});w(ue(e),t)}function g(e,t){e=ue(S(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Z(e,t){return e.substring(e.length-t.length)===t}function Y(e){const t=e.trim();if(l(t,"<")&&Z(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=S(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=Y(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),Y(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),Y(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,Y(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=ge(t,Y(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=q(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const u=p(q(t,!!n));i.push(...F(u.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=p(q(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ce(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function S(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function me(e,t,n,r){if(k(t)){return{target:te().body,event:K(e),listener:t,options:n}}else{return{target:S(e),event:K(t),listener:n,options:r}}}function ye(t,n,r,o){Gn(function(){const e=me(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function xe(t,n,r){Gn(function(){const e=me(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const be=te().createElement("output");function ve(t,n){const e=ne(t,n);if(e){if(e==="this"){return[we(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ue(A(t,function(e){return e!==t&&s(ue(e),n)}));if(i){r.push(...ve(i,n))}}if(r.length===0){H('The selector "'+e+'" on '+n+" returned no matches!");return[be]}else{return r}}}}function we(e,t){return ue(A(e,function(e){return a(ue(e),t)!=null}))}function Se(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return we(e,"hx-target")}else{return ce(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ee(e){return Q.config.attributesToSettle.includes(e)}function Ce(t,n){ie(Array.from(t.attributes),function(e){if(!n.hasAttribute(e.name)&&Ee(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ee(e.name)){t.setAttribute(e.name,e.value)}})}function Oe(t,e){const n=Jn(e);for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!Oe(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){Re(t);je(s,e,e,t,i);Te()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o,target:n})}return e}function Te(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function Re(e){ie(y(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","
");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function qe(i,e,s){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const e=p(i);const r=e&&e.querySelector(CSS.escape(t.tagName)+"#"+CSS.escape(n));if(r&&r!==e){const o=t.cloneNode();Ce(t,r);s.tasks.push(function(){Ce(t,o)})}}})}function Ae(e){return function(){b(e,Q.config.addedClass);Ft(ue(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=J(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function u(e,t,n,r){qe(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;w(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n0}function _e(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=S(h);const r=g.contextElement?q(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=P(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t0){x().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){x().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function ze(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(M(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,C)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,Ze);const l=o.length;const u=O(o,/[,\[\s]/);if(u!==""){if(u==="every"){const c={trigger:"every"};O(o,Ze);c.pollInterval=d(O(o,/[,\[\s]/));O(o,Ze);var i=nt(e,o,"event");if(i){c.eventFilter=i}r.push(c)}else{const f={trigger:u};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,Ze);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,C))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,C);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,C))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,C)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,C)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,Ze)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,Ze)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ut(e,t,n){const r=oe(e);r.timeout=x().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Xt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ut(e,t,n)}},n.pollInterval)}function ct(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ct(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ue(e);if(ft(n)){E(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"&&t.tagName==="FORM"){return true}else if(e.type==="click"){const n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit"){return true}const r=t.closest("a");const o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href"))){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,u,e,c,f){const a=oe(l);let t;if(c.from){t=m(l,c.from)}else{t=[l]}if(c.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(c)){a.lastValue.set(c,new WeakMap)}a.lastValue.get(c).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(c.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,i)){e.preventDefault()}if(pt(c,l,e)){return}const t=oe(e);t.triggerSpec=c;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(c.consume){e.stopPropagation()}if(c.target&&e.target){if(!h(ue(e.target),c.target)){return}}if(c.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(c.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(c);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(c.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");u(l,e);a.throttle=x().setTimeout(function(){a.throttle=null},c.throttle)}}else if(c.delay>0){a.delayed=x().setTimeout(function(){ae(l,"htmx:trigger");u(l,e)},c.delay)}else{ae(l,"htmx:trigger");u(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:c.trigger,listener:s,on:i});i.addEventListener(c.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&B(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){x().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(ft(n)){E(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ce(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ut(ue(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Rt(e){const t=At(e.target);const n=It(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=It(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ue(e),"button, input[type='submit']")}function Nt(e){return e.form||g(e,"form")}function It(e){const t=At(e.target);if(!t){return}const n=Nt(t);if(!n){return}return oe(n)}function Lt(e){e.addEventListener("click",Rt);e.addEventListener("focusin",Rt);e.addEventListener("focusout",qt)}function Dt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Pt(t){De(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!U()){return null}t=V(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);_e(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});$t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:_t(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){_e(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});$t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=ve(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;w(e,Q.config.requestClass)});return t}function nn(e){let t=ve(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;if(!e.hasAttribute("disabled")){e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")}});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){b(e,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0&&e.hasAttribute("data-disabled-by-htmx")){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function cn(e){if(e instanceof HTMLSelectElement&&e.multiple){return F(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return F(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,cn(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){un(e.name,cn(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){if(ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&Q.config.reportValidityOfForms){n.reportValidity()}t.push({elt:n,message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Nt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const c=s.lastButtonClicked||e;const f=ee(c,"name");ln(f,c.value,o)}const u=ve(e,"hx-include");ie(u,function(e){fn(n,r,i,ue(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Dn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=X(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=c;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{H("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;Vt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Dn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ce(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){x().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ce(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const u in n){if(n.hasOwnProperty(u)){if(i[u]==null){i[u]=n[u]}}}}return Cn(ue(c(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Hn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Tn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Rn(e,t){return le(Hn(e,t),Tn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function T(e,t){return t.test(e.getAllResponseHeaders())}function Nn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:S(r)||be,returnPromise:true})}else{let e=S(r.target);if(r.target&&!e||r.source&&!e&&!S(r.source)){e=be}return he(t,n,S(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true,push:r.push,replace:r.replace,selectOOB:r.selectOOB})}}else{return he(t,n,null,null,{returnPromise:true})}}function In(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Ln(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Dn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Pn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Pn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const M=i.handler||Vn;const F=i.select||null;if(!se(r)){re(s);return e}const u=i.targetOverride||ue(Se(r));if(u==null||u==be){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let c=oe(r);const f=c.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const N=ee(f,"formmethod");if(N!=null){if(de.includes(N.toLowerCase())){t=N}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:u,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let B=false;if(d){const I=d.split(":");const L=I[0].trim();if(L==="this"){h=we(r,"hx-sync")}else{h=ue(ce(r,L))}d=(I[1]||"drop").trim();c=oe(h);if(d==="drop"&&c.xhr&&c.abortable!==true){re(s);return e}else if(d==="abort"){if(c.xhr){re(s);return e}else{B=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(c.xhr){if(c.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const D=oe(o);if(D&&D.triggerSpec&&D.triggerSpec.queue){p=D.triggerSpec.queue}}if(p==null){p="last"}}if(c.queuedRequests==null){c.queuedRequests=[]}if(p==="first"&&c.queuedRequests.length===0){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){c.queuedRequests=[];c.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;c.xhr=g;c.abortable=B;const m=function(){c.xhr=null;c.abortable=false;if(c.queuedRequests!=null&&c.queuedRequests.length>0){const e=c.queuedRequests.shift();e()}};const X=ne(r,"hx-prompt");if(X){var y=prompt(X);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:u})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,u,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const V=U.formData;if(i.values){hn(V,Dn(i.values))}const j=Dn(Rn(r,o));const v=hn(V,j);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(u,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const $=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:$,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:u,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Dn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const _=n.split("#");const z=_[0];const O=_[1];let H=n;if(E){H=z;const Z=!w.keys().next().done;if(Z){if(H.indexOf("?")<0){H+="?"}else{H+="&"}H+=gn(w);if(O){H+="#"+O}}}if(!Ln(r,H,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),H,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const P in x){if(x.hasOwnProperty(P)){const Y=x[P];qn(g,P,Y)}}}const T={xhr:g,target:u,requestConfig:C,etc:i,boosted:$,select:F,pathInfo:{requestPath:n,finalRequestPath:H,responsePath:null,anchor:O}};g.onload=function(){try{const t=In(r);T.pathInfo.responsePath=An(g);M(r,T);if(T.keepIndicators!==true){rn(R,q)}ae(r,"htmx:afterRequest",T);ae(r,"htmx:afterOnLoad",T);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",T);ae(e,"htmx:afterOnLoad",T)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},T));throw e}finally{m()}};g.onerror=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendError",T);re(l);m()};g.onabort=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendAbort",T);re(l);m()};g.ontimeout=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:timeout",T);re(l);m()};if(!ae(r,"htmx:beforeRequest",T)){re(s);m();return e}var R=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",T);const J=E?null:wn(g,r,w);g.send(J);return e}function Mn(e,t){const n=t.xhr;let r=null;let o=null;if(T(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(T(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(T(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;let l=t.etc.push||ne(e,"hx-push-url");let u=t.etc.replace||ne(e,"hx-replace-url");if(l==="false")l=null;if(u==="false")u=null;const c=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(u){f="replace";a=u}else if(c){f="push";a=s||i}if(a){if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Fn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Bn(e){for(var t=0;t`+`.${t}{opacity:0;visibility: hidden} `+`.${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`+"")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Ft(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.detail.elt||e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};x().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/ui/src/main/resources/static/js/htmx.min.js.provenance b/ui/src/main/resources/static/js/htmx.min.js.provenance deleted file mode 100644 index 067e14c6..00000000 --- a/ui/src/main/resources/static/js/htmx.min.js.provenance +++ /dev/null @@ -1,12 +0,0 @@ -htmx 2.0.10 - -Vendored rather than fetched at runtime, so that what ships is what was reviewed. - - source https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz - path package/dist/htmx.min.js - npm sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw== - sha256 71ea67185bfa8c98c39d31717c6fce5d852370fcdfd129db4543774d3145c0de - bytes 51238 - -Verify a replacement before committing it: - sha256sum htmx.min.js diff --git a/ui/src/main/resources/static/js/window.js b/ui/src/main/resources/static/js/window.js deleted file mode 100644 index 43f53be5..00000000 --- a/ui/src/main/resources/static/js/window.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -/* Chromium owns the window contents, so the only way to reach the frame around it - is to ask the host that opened it. */ -const command = (name) => fetch("/window/" + name, { method: "POST" }); - -document.addEventListener("click", (event) => { - const button = event.target.closest("[data-window]"); - - if (button) { - command(button.dataset.window); - } -}); - -/* A page can be handed a file's contents but never its place on disk, so the host's own - dialog is asked, and the path it names is handed to the action the button carries. The - page is inert meanwhile, since the dialog is modal to the frame but not to Chromium. */ -const dialogFor = (button) => button.dataset.chooseFile !== undefined - ? "choose-file?title=" + encodeURIComponent(button.dataset.chooseFile) - : "save-file?title=" + encodeURIComponent(button.dataset.saveFile) - + "&name=" + encodeURIComponent(button.dataset.fileName || ""); - -document.addEventListener("click", async (event) => { - const button = event.target.closest("[data-choose-file], [data-save-file]"); - - if (!button) { - return; - } - - document.body.inert = true; - - try { - const response = await command(dialogFor(button)); - - if (response.status === 200) { - htmx.trigger(button, "file-chosen", { path: await response.text() }); - } - } finally { - document.body.inert = false; - } -}); - -/* Capturing keeps the release with the element even when the pointer has run ahead of - the window, which it always does during a fast drag. */ -const hold = (element, cursor, start, end) => { - let held = false; - - element.addEventListener("pointerdown", (event) => { - if (event.button !== 0 || event.target.closest("button")) { - return; - } - - held = true; - element.setPointerCapture(event.pointerId); - document.body.classList.add("is-holding"); - document.body.style.cursor = cursor; - command(start); - }); - - /* A click on a button inside is released here as well, and must not end a hold it never began. */ - const release = () => { - if (!held) { - return; - } - - held = false; - document.body.classList.remove("is-holding"); - document.body.style.cursor = ""; - command(end); - }; - - element.addEventListener("pointerup", release); - element.addEventListener("pointercancel", release); -}; - -const bar = document.querySelector("[data-drag]"); - -if (bar) { - hold(bar, "move", "move/start", "move/end"); - bar.addEventListener("dblclick", () => command("maximise")); -} - -for (const handle of document.querySelectorAll("[data-resize]")) { - hold(handle, getComputedStyle(handle).cursor, "resize/start/" + handle.dataset.resize, "resize/end"); -} - -const toggle = document.querySelector("[data-theme-toggle]"); - -if (toggle) { - toggle.addEventListener("click", () => { - const theme = document.documentElement.dataset.theme === "light" ? "dark" : "light"; - - document.documentElement.dataset.theme = theme; - localStorage.setItem("theme", theme); - }); -} diff --git a/ui/src/main/resources/templates/fragments/import.html b/ui/src/main/resources/templates/fragments/import.html index 38b41511..8572f4a8 100644 --- a/ui/src/main/resources/templates/fragments/import.html +++ b/ui/src/main/resources/templates/fragments/import.html @@ -1,7 +1,7 @@ -
+
diff --git a/ui/src/main/resources/templates/fragments/phrase.html b/ui/src/main/resources/templates/fragments/phrase.html index e840e8e2..a92a69ea 100644 --- a/ui/src/main/resources/templates/fragments/phrase.html +++ b/ui/src/main/resources/templates/fragments/phrase.html @@ -1,7 +1,7 @@ -
+
@@ -22,7 +22,7 @@

Your recovery phrase

-
+
@@ -52,7 +52,7 @@

Verify your phrase

-
+
@@ -80,7 +80,7 @@

Enter recovery phrase

-
+
diff --git a/ui/src/main/resources/templates/fragments/welcome.html b/ui/src/main/resources/templates/fragments/welcome.html index f9cc4843..c8e3a5cd 100644 --- a/ui/src/main/resources/templates/fragments/welcome.html +++ b/ui/src/main/resources/templates/fragments/welcome.html @@ -1,7 +1,7 @@ -
+
diff --git a/ui/src/main/resources/templates/index.html b/ui/src/main/resources/templates/index.html index 1f02cb89..a1a54e1f 100644 --- a/ui/src/main/resources/templates/index.html +++ b/ui/src/main/resources/templates/index.html @@ -7,12 +7,12 @@ - - + + - + - + -
+
diff --git a/ui/src/test/java/org/unigrid/janus/ui/ContainerRig.java b/ui/src/test/java/org/unigrid/janus/ui/ContainerRig.java index a3de1358..60b92b07 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/ContainerRig.java +++ b/ui/src/test/java/org/unigrid/janus/ui/ContainerRig.java @@ -30,13 +30,14 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.core.hedgehog.HedgehogStand; import org.unigrid.janus.ui.controller.WalletController; import org.unigrid.janus.ui.view.IndexView; -import org.unigrid.janus.web.RecordingWindow; -import org.unigrid.janus.web.Routes; -import org.unigrid.janus.web.SessionToken; -import org.unigrid.janus.web.Templates; -import org.unigrid.janus.web.UiServer; -import org.unigrid.janus.web.action.ActionExtension; -import org.unigrid.janus.web.action.Actions; +import org.unigrid.oriel.server.AppId; +import org.unigrid.oriel.server.Routes; +import org.unigrid.oriel.server.SessionToken; +import org.unigrid.oriel.server.UiServer; +import org.unigrid.oriel.server.action.ActionExtension; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.template.Templates; +import org.unigrid.oriel.server.window.RecordingWindow; /** * The Control Center put together the way the shell puts it together: the container builds every controller @@ -71,7 +72,7 @@ public ContainerRig() throws Exception { final Actions actions = Actions.discovered(container.getBeanManager().getExtension(ActionExtension.class)); final WalletController wallet = container.select(WalletController.class).get(); - server = new UiServer(Routes.create(new Templates(false), token, window, actions, + server = new UiServer(Routes.create(AppId.of("janus"), new Templates(false), token, window, actions, () -> new IndexView(ControlCenterRig.TITLE, wallet.start()) )); base = server.start(); diff --git a/ui/src/test/java/org/unigrid/janus/ui/ControlCenterRig.java b/ui/src/test/java/org/unigrid/janus/ui/ControlCenterRig.java index 26792350..951c37e7 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/ControlCenterRig.java +++ b/ui/src/test/java/org/unigrid/janus/ui/ControlCenterRig.java @@ -42,12 +42,13 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.ui.controller.WalletController; import org.unigrid.janus.ui.controller.WelcomeController; import org.unigrid.janus.ui.view.IndexView; -import org.unigrid.janus.web.RecordingWindow; -import org.unigrid.janus.web.Routes; -import org.unigrid.janus.web.SessionToken; -import org.unigrid.janus.web.Templates; -import org.unigrid.janus.web.UiServer; -import org.unigrid.janus.web.action.Actions; +import org.unigrid.oriel.server.AppId; +import org.unigrid.oriel.server.Routes; +import org.unigrid.oriel.server.SessionToken; +import org.unigrid.oriel.server.UiServer; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.template.Templates; +import org.unigrid.oriel.server.window.RecordingWindow; /** * The Control Center served the way the shell serves it, but kept away from the person running the @@ -93,7 +94,7 @@ public ControlCenterRig() throws Exception { ); addresses = WalletFixture.addresses(); - server = new UiServer(Routes.create(new Templates(false), token, window, actions, + server = new UiServer(Routes.create(AppId.of("janus"), new Templates(false), token, window, actions, () -> new IndexView(TITLE, wallet.start()) )); base = server.start(); diff --git a/ui/src/test/java/org/unigrid/janus/ui/LayersTest.java b/ui/src/test/java/org/unigrid/janus/ui/LayersTest.java index fbdf8daf..3b9094fd 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/LayersTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/LayersTest.java @@ -45,7 +45,7 @@ public void shouldKeepTheInterfaceOffTheServer() { @Example public void shouldKeepTheModelUnawareOfTheInterface() { noClasses().that().resideInAPackage("..janus.core..") - .should().dependOnClassesThat().resideInAnyPackage("..janus.web..", "..janus.ui..") + .should().dependOnClassesThat().resideInAnyPackage("org.unigrid.oriel..", "..janus.ui..") .check(CLASSES); } } diff --git a/ui/src/test/java/org/unigrid/janus/ui/Screen.java b/ui/src/test/java/org/unigrid/janus/ui/Screen.java index 4082a50b..77f13b26 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/Screen.java +++ b/ui/src/test/java/org/unigrid/janus/ui/Screen.java @@ -31,7 +31,7 @@ of the License (see COPYING and COPYING.addendum). import org.jsoup.nodes.Attribute; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; -import org.unigrid.janus.web.Client; +import org.unigrid.oriel.server.Client; /** * The page as the window holds it, changed the way htmx would change it when a control is used. diff --git a/ui/src/test/java/org/unigrid/janus/ui/controller/AboutControllerTest.java b/ui/src/test/java/org/unigrid/janus/ui/controller/AboutControllerTest.java index e501fb35..0f6ed9f6 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/controller/AboutControllerTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/controller/AboutControllerTest.java @@ -21,10 +21,10 @@ of the License (see COPYING and COPYING.addendum). import net.jqwik.api.Example; import org.unigrid.janus.core.Release; import org.unigrid.janus.ui.view.AboutView; -import org.unigrid.janus.web.action.ActionExtension; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.Form; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.ActionExtension; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.action.Form; +import org.unigrid.oriel.server.action.View; import static org.junit.jupiter.api.Assertions.assertEquals; public class AboutControllerTest { diff --git a/ui/src/test/java/org/unigrid/janus/ui/controller/ImportControllerTest.java b/ui/src/test/java/org/unigrid/janus/ui/controller/ImportControllerTest.java index a8b2f8bd..1a8bd332 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/controller/ImportControllerTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/controller/ImportControllerTest.java @@ -38,10 +38,10 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.core.legacy.LegacyKey; import org.unigrid.janus.ui.WalletFixture; import org.unigrid.janus.ui.view.ImportView; -import org.unigrid.janus.web.action.ActionExtension; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.Form; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.ActionExtension; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.action.Form; +import org.unigrid.oriel.server.action.View; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; diff --git a/ui/src/test/java/org/unigrid/janus/ui/controller/WalletControllerTest.java b/ui/src/test/java/org/unigrid/janus/ui/controller/WalletControllerTest.java index b437e02b..923f7319 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/controller/WalletControllerTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/controller/WalletControllerTest.java @@ -44,18 +44,18 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.core.wallet.WalletLedger; import org.unigrid.janus.ui.view.ActivityView; import org.unigrid.janus.ui.view.AppView; -import org.unigrid.janus.ui.view.DashboardView; import org.unigrid.janus.ui.view.DashboardView.FundedPage; import org.unigrid.janus.ui.view.DashboardView.Holding; +import org.unigrid.janus.ui.view.DashboardView; import org.unigrid.janus.ui.view.ExportNoteView; import org.unigrid.janus.ui.view.ImportView; import org.unigrid.janus.ui.view.PreparingView; import org.unigrid.janus.ui.view.RowView; import org.unigrid.janus.ui.view.RowsView; import org.unigrid.janus.ui.view.WelcomeView; -import org.unigrid.janus.web.action.ActionExtension; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.Form; +import org.unigrid.oriel.server.action.ActionExtension; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.action.Form; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/ui/src/test/java/org/unigrid/janus/ui/controller/WelcomeControllerTest.java b/ui/src/test/java/org/unigrid/janus/ui/controller/WelcomeControllerTest.java index fd8b40c8..e35ef4de 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/controller/WelcomeControllerTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/controller/WelcomeControllerTest.java @@ -20,10 +20,10 @@ of the License (see COPYING and COPYING.addendum). import jakarta.enterprise.inject.se.SeContainerInitializer; import net.jqwik.api.Example; import org.unigrid.janus.ui.view.WelcomeView; -import org.unigrid.janus.web.action.ActionExtension; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.Form; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.ActionExtension; +import org.unigrid.oriel.server.action.Actions; +import org.unigrid.oriel.server.action.Form; +import org.unigrid.oriel.server.action.View; import static org.junit.jupiter.api.Assertions.assertEquals; public class WelcomeControllerTest { diff --git a/ui/src/test/java/org/unigrid/janus/ui/view/ActivityViewTest.java b/ui/src/test/java/org/unigrid/janus/ui/view/ActivityViewTest.java index 62a430f5..5d88b901 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/view/ActivityViewTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/view/ActivityViewTest.java @@ -21,8 +21,8 @@ of the License (see COPYING and COPYING.addendum). import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.unigrid.janus.ui.view.ActivityView.Summary; -import org.unigrid.janus.web.Templates; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; +import org.unigrid.oriel.server.template.Templates; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/ui/src/test/java/org/unigrid/janus/ui/view/AppViewsTest.java b/ui/src/test/java/org/unigrid/janus/ui/view/AppViewsTest.java index a93908b7..f9dded12 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/view/AppViewsTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/view/AppViewsTest.java @@ -23,7 +23,7 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.ui.view.AppView.Tab; import org.unigrid.janus.ui.view.PreparingView.Step; import org.unigrid.janus.ui.view.PreparingView.Step.State; -import org.unigrid.janus.web.Templates; +import org.unigrid.oriel.server.template.Templates; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/ui/src/test/java/org/unigrid/janus/ui/view/DashboardViewTest.java b/ui/src/test/java/org/unigrid/janus/ui/view/DashboardViewTest.java index 21b83658..fa7cea26 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/view/DashboardViewTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/view/DashboardViewTest.java @@ -25,7 +25,7 @@ of the License (see COPYING and COPYING.addendum). import org.unigrid.janus.ui.view.DashboardView.Bar; import org.unigrid.janus.ui.view.DashboardView.FundedPage; import org.unigrid.janus.ui.view.DashboardView.Holding; -import org.unigrid.janus.web.Templates; +import org.unigrid.oriel.server.template.Templates; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/ui/src/test/java/org/unigrid/janus/ui/view/RowViewTest.java b/ui/src/test/java/org/unigrid/janus/ui/view/RowViewTest.java index c19303a3..48972011 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/view/RowViewTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/view/RowViewTest.java @@ -20,8 +20,8 @@ of the License (see COPYING and COPYING.addendum). import net.jqwik.api.Example; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; -import org.unigrid.janus.web.Templates; -import org.unigrid.janus.web.action.View; +import org.unigrid.oriel.server.action.View; +import org.unigrid.oriel.server.template.Templates; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/ui/src/test/java/org/unigrid/janus/ui/view/ViewsTest.java b/ui/src/test/java/org/unigrid/janus/ui/view/ViewsTest.java index 2677336a..011d3f15 100644 --- a/ui/src/test/java/org/unigrid/janus/ui/view/ViewsTest.java +++ b/ui/src/test/java/org/unigrid/janus/ui/view/ViewsTest.java @@ -19,7 +19,7 @@ of the License (see COPYING and COPYING.addendum). import java.nio.file.Path; import java.util.regex.Pattern; import net.jqwik.api.Example; -import org.unigrid.janus.web.Templates; +import org.unigrid.oriel.server.template.Templates; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/web/pom.xml b/web/pom.xml deleted file mode 100644 index 788c9189..00000000 --- a/web/pom.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - 4.0.0 - - org.unigrid.janus - parent - 2.0.1-SNAPSHOT - - web - jar - Unigrid Janus Web - Local HTTP server, request handlers and template engine behind the user interface. - - - org.unigrid.janus - core - ${project.version} - - - org.projectlombok - lombok - provided - - - jakarta.enterprise - jakarta.enterprise.cdi-api - - - org.eclipse.jetty - jetty-server - - - org.thymeleaf - thymeleaf - - - org.slf4j - slf4j-api - - - org.junit.jupiter - junit-jupiter - - - net.jqwik - jqwik - - - com.tngtech.archunit - archunit - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.jacoco - jacoco-maven-plugin - - - - org.apache.maven.plugins - maven-jar-plugin - - - - test-jar - - - - logback-test.xml - - - - - - - - diff --git a/web/src/main/java/org/unigrid/janus/web/ActionHandler.java b/web/src/main/java/org/unigrid/janus/web/ActionHandler.java deleted file mode 100644 index 9006e40c..00000000 --- a/web/src/main/java/org/unigrid/janus/web/ActionHandler.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; -import org.unigrid.janus.web.action.ActionFailed; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.Form; -import org.unigrid.janus.web.action.View; - -@Slf4j -public class ActionHandler extends Handler.Abstract { - private static final String HTML = "text/html;charset=utf-8"; - - private final Actions actions; - private final Templates templates; - - public ActionHandler(final Actions actions, final Templates templates) { - this.actions = actions; - this.templates = templates; - } - - @Override - public boolean handle(final Request request, final Response response, final Callback callback) - throws Exception { - - final String name = Request.getPathInContext(request).substring(1); - - /* Unknown actions are refused rather than passed on. A page falling back to the whole - interface when a swap was expected hides the mistake instead of showing it. */ - if (!actions.knows(name)) { - response.setStatus(HttpStatus.NOT_FOUND_404); - callback.succeeded(); - return true; - } - - final Optional shown; - - try { - shown = actions.invoke(name, Form.parse(Content.Source.asString(request))); - } catch (ActionFailed e) { - /* The page only learns that something failed. Why is kept here, where it can be found afterwards. */ - log.error("The action {} failed", name, e.getCause()); - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); - callback.succeeded(); - return true; - } - - if (shown.isEmpty()) { - response.setStatus(HttpStatus.NO_CONTENT_204); - callback.succeeded(); - return true; - } - - final byte[] body = templates.render(shown.get()).getBytes(StandardCharsets.UTF_8); - - response.setStatus(HttpStatus.OK_200); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, HTML); - response.write(true, ByteBuffer.wrap(body), callback); - return true; - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/Guard.java b/web/src/main/java/org/unigrid/janus/web/Guard.java deleted file mode 100644 index a329cd20..00000000 --- a/web/src/main/java/org/unigrid/janus/web/Guard.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.HttpCookie; -import java.util.List; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; - -/** - * Refuses anything that cannot prove it is the interface this process opened. A loopback port is - * reachable by every other program on the machine, and this one can move money. - */ -public class Guard extends Handler.Wrapper { - private final SessionToken token; - - public Guard(final Handler handler, final SessionToken token) { - super(handler); - this.token = token; - } - - @Override - public boolean handle(final Request request, final Response response, final Callback callback) - throws Exception { - - if (foreignOrigin(request)) { - return refuse(response, callback); - } - - if (token.matches(cookie(request))) { - return super.handle(request, response, callback); - } - - /* The very first navigation carries the token in the query, because there is nowhere - else to put it before a cookie exists. It is exchanged for a cookie immediately so - that it does not stay in the address or in referrers. */ - if (token.matches(Request.extractQueryParameters(request).getValue(SessionToken.PARAMETER))) { - return adopt(request, response, callback); - } - - return refuse(response, callback); - } - - private boolean adopt(final Request request, final Response response, final Callback callback) { - response.getHeaders().put(HttpHeader.SET_COOKIE, - SessionToken.COOKIE + "=" + token.value() + "; Path=/; HttpOnly; SameSite=Strict" - ); - - Response.sendRedirect(request, response, callback, Request.getPathInContext(request)); - return true; - } - - private boolean refuse(final Response response, final Callback callback) { - response.setStatus(HttpStatus.FORBIDDEN_403); - callback.succeeded(); - return true; - } - - private String cookie(final Request request) { - for (final String header : request.getHeaders().getValuesList(HttpHeader.COOKIE)) { - for (final HttpCookie cookie : parse(header)) { - if (SessionToken.COOKIE.equals(cookie.getName())) { - return cookie.getValue(); - } - } - } - - return null; - } - - private List parse(final String header) { - try { - return HttpCookie.parse(header); - } catch (IllegalArgumentException _) { - return List.of(); - } - } - - /* A page in the user's own browser can reach this port. It cannot read the responses, but - without this it could still fire requests that act. */ - private boolean foreignOrigin(final Request request) { - final String origin = request.getHeaders().get(HttpHeader.ORIGIN); - - return origin != null && !origin.equals(expectedOrigin(request)); - } - - private String expectedOrigin(final Request request) { - return "http://" + request.getHeaders().get(HttpHeader.HOST); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/Routes.java b/web/src/main/java/org/unigrid/janus/web/Routes.java deleted file mode 100644 index a4d230a8..00000000 --- a/web/src/main/java/org/unigrid/janus/web/Routes.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.util.function.Supplier; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.ResourceHandler; -import org.eclipse.jetty.util.resource.ResourceFactory; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.View; - -public final class Routes { - private static final String ASSETS = "static/"; - private static final String ASSET_PATH = "/static"; - private static final String WINDOW_PATH = "/window"; - private static final String ACTION_PATH = "/action"; - - private Routes() { - } - - public static Handler create(final Templates templates, final SessionToken token, final View index) { - return create(templates, token, WindowControl.NONE, Actions.of(), index); - } - - public static Handler create(final Templates templates, final SessionToken token, - final WindowControl window, final Actions actions, final View index) { - - return create(templates, token, window, actions, () -> index); - } - - public static Handler create(final Templates templates, final SessionToken token, - final WindowControl window, final Actions actions, final Supplier index) { - - final ResourceHandler assets = new ResourceHandler(); - - assets.setBaseResource(ResourceFactory.of(assets).newClassLoaderResource(ASSETS)); - assets.setDirAllowed(false); - - /* Anything the asset handler does not recognise falls through to the pages, - so an unknown path renders the interface rather than a Jetty error page. */ - return new Guard(new Handler.Sequence( - new ContextHandler(assets, ASSET_PATH), - new ContextHandler(new WindowHandler(window), WINDOW_PATH), - new ContextHandler(new ActionHandler(actions, templates), ACTION_PATH), - new UiHandler(templates, index) - ), token); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/SessionToken.java b/web/src/main/java/org/unigrid/janus/web/SessionToken.java deleted file mode 100644 index fc7754ee..00000000 --- a/web/src/main/java/org/unigrid/janus/web/SessionToken.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Base64; -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Proof that a request came from the interface this process opened, rather than from anything - * else that happens to be running on the machine and can reach a loopback port. - */ -public final class SessionToken { - public static final String COOKIE = "janus-session"; - public static final String PARAMETER = "t"; - - private static final int LENGTH = 32; - - private final String value; - - private SessionToken(final String value) { - this.value = value; - } - - public static SessionToken random() { - final byte[] bytes = new byte[LENGTH]; - - new SecureRandom().nextBytes(bytes); - return new SessionToken(Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)); - } - - public String value() { - return value; - } - - public boolean matches(final String candidate) { - if (candidate == null) { - return false; - } - - /* Constant time, so that a caller cannot learn the token one character at a time - by measuring how long the comparison takes. */ - return MessageDigest.isEqual(candidate.getBytes(UTF_8), value.getBytes(UTF_8)); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/Templates.java b/web/src/main/java/org/unigrid/janus/web/Templates.java deleted file mode 100644 index 496400e5..00000000 --- a/web/src/main/java/org/unigrid/janus/web/Templates.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.util.Set; -import org.thymeleaf.TemplateEngine; -import org.thymeleaf.context.Context; -import org.thymeleaf.templatemode.TemplateMode; -import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; -import org.unigrid.janus.web.action.View; - -public class Templates { - private static final String PREFIX = "templates/"; - private static final String SUFFIX = ".html"; - private static final String SELECTOR = "::"; - private static final String VARIABLE = "view"; - - private final TemplateEngine engine = new TemplateEngine(); - - public Templates(final boolean cache) { - final ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); - - resolver.setPrefix(PREFIX); - resolver.setSuffix(SUFFIX); - resolver.setTemplateMode(TemplateMode.HTML); - resolver.setCharacterEncoding("UTF-8"); - - /* Development switches caching off so an edited template appears on the next - request, which is the entire point of building the interface in a browser. */ - resolver.setCacheable(cache); - engine.setTemplateResolver(resolver); - } - - /** - * Renders what the view names, the way Thymeleaf names it: {@code page} or {@code page :: block}. - * The view itself is what the template reads as {@code view}. - */ - public String render(final View view) { - final String specification = view.template(); - final int separator = specification.indexOf(SELECTOR); - final Context context = new Context(); - - context.setVariable(VARIABLE, view); - - if (separator < 0) { - return engine.process(specification.trim(), context); - } - - return engine.process(specification.substring(0, separator).trim(), - Set.of(specification.substring(separator + SELECTOR.length()).trim()), context - ); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/UiHandler.java b/web/src/main/java/org/unigrid/janus/web/UiHandler.java deleted file mode 100644 index 63a2189f..00000000 --- a/web/src/main/java/org/unigrid/janus/web/UiHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.function.Supplier; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; -import org.unigrid.janus.web.action.View; - -public class UiHandler extends Handler.Abstract { - private static final String HTML = "text/html;charset=utf-8"; - - private final Templates templates; - private final Supplier index; - - public UiHandler(final Templates templates, final Supplier index) { - this.templates = templates; - this.index = index; - } - - @Override - public boolean handle(final Request request, final Response response, final Callback callback) { - final ByteBuffer body = ByteBuffer.wrap(templates.render(index.get()).getBytes(StandardCharsets.UTF_8)); - - response.setStatus(HttpStatus.OK_200); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, HTML); - response.write(true, body, callback); - return true; - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/UiServer.java b/web/src/main/java/org/unigrid/janus/web/UiServer.java deleted file mode 100644 index d272e4b5..00000000 --- a/web/src/main/java/org/unigrid/janus/web/UiServer.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.URI; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; - -public class UiServer { - /* The wallet holds keys, so the interface is never reachable from off the machine. - Port zero keeps it off a predictable address that another local process could - guess ahead of time. */ - private static final String LOOPBACK = "127.0.0.1"; - private static final int EPHEMERAL = 0; - - private final Server server = new Server(); - private final ServerConnector connector = new ServerConnector(server); - - public UiServer(final Handler handler) { - connector.setHost(LOOPBACK); - connector.setPort(EPHEMERAL); - server.addConnector(connector); - server.setHandler(handler); - } - - public URI start() throws Exception { - server.start(); - return URI.create("http://" + LOOPBACK + ":" + connector.getLocalPort() + "/"); - } - - public void stop() throws Exception { - server.stop(); - } - - public void join() throws InterruptedException { - server.join(); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/WindowControl.java b/web/src/main/java/org/unigrid/janus/web/WindowControl.java deleted file mode 100644 index 643b2386..00000000 --- a/web/src/main/java/org/unigrid/janus/web/WindowControl.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Locale; -import java.util.Optional; - -/** - * The operations a page cannot perform on its own window. Implemented by whatever hosts the - * interface, so that the pages stay unaware of whether they are in a frame or a browser tab. - */ -public interface WindowControl { - WindowControl NONE = new WindowControl() { }; - - /** The edges a window can be resized from. The top is left to the title bar. */ - enum Edge { - LEFT, RIGHT, BOTTOM, BOTTOM_RIGHT; - - /** The edge a page names in a path, as in {@code bottom-right}. */ - public static Optional named(final String name) { - return Arrays.stream(values()).filter(edge -> edge.pathName().equals(name)).findFirst(); - } - - public String pathName() { - return name().toLowerCase(Locale.ROOT).replace('_', '-'); - } - } - - default void minimise() { - } - - default void toggleMaximise() { - } - - default void close() { - } - - /** - * Begins moving the window with the pointer. Only the start and the end are reported, because - * a request per pointer movement would make dragging as slow as the round trip. - */ - default void beginMove() { - } - - default void endMove() { - } - - /** Begins resizing the window from an edge with the pointer, reported like a move. */ - default void beginResize(final Edge edge) { - } - - default void endResize() { - } - - /** - * Asks the person for a file with the host's own dialog and answers with where it is. A page - * can only ever be handed a file's contents, never its place on disk, and a wallet is too - * large to copy through a request. Empty when the dialog was dismissed. - */ - default Optional chooseFile(final String title) { - return Optional.empty(); - } - - /** Asks the person where to save a file, suggesting a name, with the host's own dialog. Empty when dismissed. */ - default Optional saveFile(final String title, final String name) { - return Optional.empty(); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/WindowHandler.java b/web/src/main/java/org/unigrid/janus/web/WindowHandler.java deleted file mode 100644 index 68071531..00000000 --- a/web/src/main/java/org/unigrid/janus/web/WindowHandler.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Objects; -import java.util.Optional; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.Fields; -import org.unigrid.janus.web.WindowControl.Edge; - -public class WindowHandler extends Handler.Abstract { - private static final String RESIZE_START = "resize/start/"; - private static final String CHOOSE_FILE = "choose-file"; - private static final String SAVE_FILE = "save-file"; - private static final String NAME = "name"; - private static final String TITLE = "title"; - private static final String TEXT = "text/plain;charset=utf-8"; - - private final WindowControl window; - - public WindowHandler(final WindowControl window) { - this.window = window; - } - - @Override - public boolean handle(final Request request, final Response response, final Callback callback) { - final String command = Request.getPathInContext(request).substring(1); - final Fields query = Request.extractQueryParameters(request); - - if (CHOOSE_FILE.equals(command)) { - return answerWith(window.chooseFile(value(query, TITLE)), response, callback); - } - - if (SAVE_FILE.equals(command)) { - return answerWith(window.saveFile(value(query, TITLE), value(query, NAME)), response, callback); - } - - if (!perform(command)) { - return false; - } - - response.setStatus(HttpStatus.NO_CONTENT_204); - callback.succeeded(); - return true; - } - - private static String value(final Fields query, final String name) { - return Objects.requireNonNullElse(query.getValue(name), ""); - } - - private static boolean answerWith(final Optional chosen, final Response response, final Callback callback) { - if (chosen.isEmpty()) { - response.setStatus(HttpStatus.NO_CONTENT_204); - callback.succeeded(); - return true; - } - - response.setStatus(HttpStatus.OK_200); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, TEXT); - response.write(true, ByteBuffer.wrap(chosen.get().toString().getBytes(StandardCharsets.UTF_8)), callback); - return true; - } - - private boolean perform(final String command) { - if (command.startsWith(RESIZE_START)) { - return beginResize(command.substring(RESIZE_START.length())); - } - - switch (command) { - case "minimise" -> window.minimise(); - case "maximise" -> window.toggleMaximise(); - case "close" -> window.close(); - case "move/start" -> window.beginMove(); - case "move/end" -> window.endMove(); - case "resize/end" -> window.endResize(); - default -> { - return false; - } - } - - return true; - } - - private boolean beginResize(final String name) { - final Optional edge = Edge.named(name); - - edge.ifPresent(window::beginResize); - return edge.isPresent(); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/action/Action.java b/web/src/main/java/org/unigrid/janus/web/action/Action.java deleted file mode 100644 index 02109db7..00000000 --- a/web/src/main/java/org/unigrid/janus/web/action/Action.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web.action; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Binds a method to something the interface can ask for by name, so that markup naming an action - * and the method carrying it out can be read against each other. - * - *

The method takes either nothing or a {@link Form}. Returning a {@link View} renders it for - * the page to swap in; returning nothing tells the page that there is nothing to change. - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface Action { - String value(); -} diff --git a/web/src/main/java/org/unigrid/janus/web/action/ActionExtension.java b/web/src/main/java/org/unigrid/janus/web/action/ActionExtension.java deleted file mode 100644 index b980ae80..00000000 --- a/web/src/main/java/org/unigrid/janus/web/action/ActionExtension.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web.action; - -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.spi.AnnotatedMethod; -import jakarta.enterprise.inject.spi.Extension; -import jakarta.enterprise.inject.spi.ProcessAnnotatedType; -import java.lang.reflect.Method; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Collects every {@link Action} the container sees while it starts, so that adding one is a matter - * of annotating a method rather than remembering to register it somewhere else as well. - */ -public class ActionExtension implements Extension { - private final Map found = new LinkedHashMap<>(); - - void collect(@Observes final ProcessAnnotatedType event) { - for (final AnnotatedMethod method : event.getAnnotatedType().getMethods()) { - Actions.bind(found, method.getJavaMember()); - } - } - - public Map found() { - return Map.copyOf(found); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/action/ActionFailed.java b/web/src/main/java/org/unigrid/janus/web/action/ActionFailed.java deleted file mode 100644 index 0c709a73..00000000 --- a/web/src/main/java/org/unigrid/janus/web/action/ActionFailed.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web.action; - -public class ActionFailed extends RuntimeException { - private static final long serialVersionUID = 1L; - - public ActionFailed(final String action, final Throwable cause) { - super("The action " + action + " did not complete", cause); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/action/Actions.java b/web/src/main/java/org/unigrid/janus/web/action/Actions.java deleted file mode 100644 index 320a2156..00000000 --- a/web/src/main/java/org/unigrid/janus/web/action/Actions.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web.action; - -import jakarta.enterprise.inject.spi.CDI; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; - -/** What the interface is allowed to ask for, and the methods that carry it out. */ -public final class Actions { - private final Map bound; - private final Function, Object> resolve; - - private Actions(final Map bound, final Function, Object> resolve) { - this.bound = bound; - this.resolve = resolve; - } - - /** Everything the container found, invoked against the beans it manages. */ - public static Actions discovered(final ActionExtension extension) { - return new Actions(extension.found(), type -> CDI.current().select(type).get()); - } - - /** Bound to the given objects, for callers that have no container. */ - public static Actions of(final Object... handlers) { - final Map bound = new LinkedHashMap<>(); - final Map, Object> instances = new LinkedHashMap<>(); - - for (final Object handler : handlers) { - instances.put(handler.getClass(), handler); - - for (final Method method : handler.getClass().getMethods()) { - bind(bound, method); - } - } - - return new Actions(bound, instances::get); - } - - /** - * Remembers the action the method carries out, refusing a name that another method already - * answers to. Which of them the interface reached would otherwise depend on the order they - * happened to be scanned in. - */ - static void bind(final Map bound, final Method method) { - final Action action = method.getAnnotation(Action.class); - - if (action == null) { - return; - } - - final Method taken = bound.putIfAbsent(action.value(), method); - - if (taken != null) { - throw new IllegalStateException("The action " + action.value() + " is bound to both " - + describe(taken) + " and " + describe(method) - ); - } - } - - private static String describe(final Method method) { - return method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; - } - - public boolean knows(final String name) { - return bound.containsKey(name); - } - - private static View asView(final String name, final Object outcome) { - if (outcome instanceof View view) { - return view; - } - - throw new ActionFailed(name, new IllegalStateException(outcome.getClass().getName() + " is not a view")); - } - - /** - * Carries out the named action, answering with the view the page should show, or nothing - * when there is nothing to change. - */ - public Optional invoke(final String name, final Form form) { - final Method method = bound.get(name); - - if (method == null) { - throw new IllegalArgumentException("No action named " + name); - } - - try { - final Object target = resolve.apply(method.getDeclaringClass()); - final Object outcome = method.getParameterCount() == 0 - ? method.invoke(target) - : method.invoke(target, form); - - return Optional.ofNullable(outcome).map(o -> asView(name, o)); - } catch (IllegalAccessException e) { - throw new ActionFailed(name, e); - } catch (InvocationTargetException e) { - /* The method itself failed. A wallet must not answer as though nothing went - wrong, so the cause travels up rather than being swallowed here. */ - throw new ActionFailed(name, e.getCause()); - } - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/action/Form.java b/web/src/main/java/org/unigrid/janus/web/action/Form.java deleted file mode 100644 index 36d145d7..00000000 --- a/web/src/main/java/org/unigrid/janus/web/action/Form.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web.action; - -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.Map; - -/** The values a form carried, so that an action reads what it was given rather than a request. */ -public final class Form { - private static final String PAIRS = "&"; - private static final String ASSIGN = "="; - - private final Map values; - - private Form(final Map values) { - this.values = values; - } - - public static Form parse(final String body) { - final Map values = new LinkedHashMap<>(); - - if (body != null && !body.isBlank()) { - for (final String pair : body.split(PAIRS)) { - final int at = pair.indexOf(ASSIGN); - - if (at > 0) { - values.put(decode(pair.substring(0, at)), decode(pair.substring(at + 1))); - } - } - } - - return new Form(values); - } - - public String get(final String name) { - return values.get(name); - } - - public boolean has(final String name) { - final String value = values.get(name); - - return value != null && !value.isEmpty(); - } - - private static String decode(final String value) { - return URLDecoder.decode(value, StandardCharsets.UTF_8); - } -} diff --git a/web/src/main/java/org/unigrid/janus/web/action/View.java b/web/src/main/java/org/unigrid/janus/web/action/View.java deleted file mode 100644 index cfdd8653..00000000 --- a/web/src/main/java/org/unigrid/janus/web/action/View.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web.action; - -/** - * What a controller wants shown. The template reads the object itself as {@code view}, so a - * template and the record behind it can be read against each other. - */ -public interface View { - /** The template, named the way Thymeleaf names one: {@code page} or {@code page :: block}. */ - String template(); -} diff --git a/web/src/main/resources/META-INF/beans.xml b/web/src/main/resources/META-INF/beans.xml deleted file mode 100644 index 1f4584c6..00000000 --- a/web/src/main/resources/META-INF/beans.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/web/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension b/web/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension deleted file mode 100644 index c31ef72c..00000000 --- a/web/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension +++ /dev/null @@ -1 +0,0 @@ -org.unigrid.janus.web.action.ActionExtension diff --git a/web/src/test/java/org/unigrid/janus/web/ActionHandlerTest.java b/web/src/test/java/org/unigrid/janus/web/ActionHandlerTest.java deleted file mode 100644 index bfe56bc1..00000000 --- a/web/src/test/java/org/unigrid/janus/web/ActionHandlerTest.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; -import java.net.http.HttpResponse; -import java.util.ArrayList; -import java.util.List; -import net.jqwik.api.Example; -import org.eclipse.jetty.server.Handler; -import org.slf4j.LoggerFactory; -import org.unigrid.janus.web.action.Action; -import org.unigrid.janus.web.action.Actions; -import org.unigrid.janus.web.action.Form; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ActionHandlerTest extends ServedTest { - private final Wallet wallet = new Wallet(); - - public class Wallet { - private final List unlocked = new ArrayList<>(); - - @Action("lock-wallet") - public void onClickLockWallet() { - unlocked.clear(); - } - - @Action("unlock-wallet") - public Status onClickUnlockWallet(final Form form) { - unlocked.add(form.get("passphrase")); - return new Status(form.get("passphrase")); - } - - @Action("break-wallet") - public void onClickBreakWallet() { - throw new IllegalStateException("Hedgehog said no"); - } - - @Action("misreport-wallet") - public String onClickMisreportWallet() { - return "not a view"; - } - - public List unlocked() { - return unlocked; - } - } - - @Override - protected Handler routes() { - return Routes.create(templates(), token(), WindowControl.NONE, Actions.of(wallet), new Page("Janus")); - } - - @Example - public void shouldCallTheAnnotatedMethod() throws Exception { - assertEquals(204, admitted().post("/action/lock-wallet").statusCode()); - } - - @Example - public void shouldHandTheFormToTheMethod() throws Exception { - final HttpResponse response = admitted() - .submit("/action/unlock-wallet", "passphrase=open+sesame%21"); - - assertEquals(200, response.statusCode()); - assertEquals(List.of("open sesame!"), wallet.unlocked()); - } - - @Example - public void shouldRenderTheViewThatWasReturned() throws Exception { - final HttpResponse response = admitted() - .submit("/action/unlock-wallet", "passphrase=x"); - - assertTrue(response.body().contains("Janus is unlocked"), response.body()); - assertTrue(!response.body().contains(""), - "a fragment should be returned rather than the whole page: " + response.body() - ); - } - - @Example - public void shouldRefuseAnActionItDoesNotHave() throws Exception { - assertEquals(404, admitted().post("/action/drain-wallet").statusCode()); - } - - @Example - public void shouldNotActForAnUnknownCaller() throws Exception { - assertEquals(403, anonymous().post("/action/lock-wallet").statusCode()); - } - - @Example - public void shouldReportAFailedActionRatherThanPretendItWorked() throws Exception { - assertEquals(500, admitted().post("/action/break-wallet").statusCode()); - } - - @Example - public void shouldLogWhyAnActionFailed() throws Exception { - final Logger logger = (Logger) LoggerFactory.getLogger(ActionHandler.class); - final ListAppender logged = new ListAppender<>(); - - logged.start(); - logger.addAppender(logged); - - try { - admitted().post("/action/break-wallet"); - } finally { - logger.detachAppender(logged); - } - - assertEquals(1, logged.list.size()); - assertEquals(Level.ERROR, logged.list.get(0).getLevel()); - assertTrue(logged.list.get(0).getFormattedMessage().contains("break-wallet")); - assertEquals("Hedgehog said no", logged.list.get(0).getThrowableProxy().getMessage()); - } - - @Example - public void shouldReportAnActionThatAnswersWithSomethingOtherThanAView() throws Exception { - assertEquals(500, admitted().post("/action/misreport-wallet").statusCode()); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/Client.java b/web/src/test/java/org/unigrid/janus/web/Client.java deleted file mode 100644 index 31f8eb93..00000000 --- a/web/src/test/java/org/unigrid/janus/web/Client.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.CookieManager; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.LinkedHashMap; -import java.util.Map; - -/** A caller of the interface, keeping its own cookies so that admitted and unknown callers differ. */ -public class Client { - private static final String FORM = "application/x-www-form-urlencoded"; - - private final HttpClient http; - private final URI base; - private final Map headers; - - public Client(final URI base) { - this(HttpClient.newBuilder().cookieHandler(new CookieManager()) - .followRedirects(HttpClient.Redirect.NORMAL).build(), base, Map.of() - ); - } - - private Client(final HttpClient http, final URI base, final Map headers) { - this.http = http; - this.base = base; - this.headers = headers; - } - - public Client header(final String name, final String value) { - final Map combined = new LinkedHashMap<>(headers); - - combined.put(name, value); - return new Client(http, base, combined); - } - - public HttpResponse get(final String path) throws Exception { - return send(request(path).GET()); - } - - public HttpResponse post(final String path) throws Exception { - return send(request(path).POST(HttpRequest.BodyPublishers.noBody())); - } - - public HttpResponse submit(final String path, final String form) throws Exception { - return send(request(path).header("Content-Type", FORM).POST(HttpRequest.BodyPublishers.ofString(form))); - } - - private HttpRequest.Builder request(final String path) { - final HttpRequest.Builder builder = HttpRequest.newBuilder(base.resolve(path)); - - headers.forEach(builder::header); - return builder; - } - - private HttpResponse send(final HttpRequest.Builder builder) throws Exception { - return http.send(builder.build(), HttpResponse.BodyHandlers.ofString()); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/GuardTest.java b/web/src/test/java/org/unigrid/janus/web/GuardTest.java deleted file mode 100644 index be14109c..00000000 --- a/web/src/test/java/org/unigrid/janus/web/GuardTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.http.HttpResponse; -import net.jqwik.api.Example; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class GuardTest extends ServedTest { - @Example - public void shouldRefuseARequestWithoutTheToken() throws Exception { - final HttpResponse response = anonymous().get("/"); - - assertEquals(403, response.statusCode()); - } - - @Example - public void shouldRefuseAWrongToken() throws Exception { - final HttpResponse response = anonymous().get("/?t=not-the-token"); - - assertEquals(403, response.statusCode()); - } - - @Example - public void shouldExchangeTheQueryTokenForACookie() throws Exception { - final Client client = anonymous(); - final HttpResponse bootstrap = client.get("/?" + SessionToken.PARAMETER + "=" + token().value()); - - assertEquals(200, bootstrap.statusCode(), "the redirect should have been followed to the page"); - assertTrue(bootstrap.body().contains("

Janus

"), bootstrap.body()); - assertEquals(200, client.get("/").statusCode(), "the cookie should carry the next request"); - } - - @Example - public void shouldRefuseARequestSentFromSomebodyElsesPage() throws Exception { - final HttpResponse response = admitted() - .header("Origin", "https://unigrid.example") - .get("/window/close"); - - assertEquals(403, response.statusCode()); - } - - @Example - public void shouldGuardTheAssetsToo() throws Exception { - assertEquals(403, anonymous().get("/static/css/testing.css").statusCode()); - assertEquals(200, admitted().get("/static/css/testing.css").statusCode()); - } - - @Example - public void shouldIssueADifferentTokenEachTime() { - assertTrue(!SessionToken.random().value().equals(SessionToken.random().value())); - } - - @Example - public void shouldRejectAnAbsentCandidate() { - assertTrue(!token().matches(null)); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/Page.java b/web/src/test/java/org/unigrid/janus/web/Page.java deleted file mode 100644 index 99ae5aa3..00000000 --- a/web/src/test/java/org/unigrid/janus/web/Page.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import org.unigrid.janus.web.action.View; - -/** The front page as the tests know it. */ -public record Page(String title) implements View { - @Override - public String template() { - return "page"; - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/RecordingWindow.java b/web/src/test/java/org/unigrid/janus/web/RecordingWindow.java deleted file mode 100644 index 5467356b..00000000 --- a/web/src/test/java/org/unigrid/janus/web/RecordingWindow.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.nio.file.Path; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * A host that remembers every command it was given, in the words the page uses for them, and - * answers a file dialog with whatever file it was told the person picked. Commands arrive on the - * server's threads, so a caller that has to wait for them does so with {@link #await}. - */ -public class RecordingWindow implements WindowControl { - private static final long POLL_MILLIS = 10; - - private final List commands = new CopyOnWriteArrayList<>(); - private volatile Path picked; - - public void picking(final Path file) { - picked = file; - } - - public List commands() { - return List.copyOf(commands); - } - - /** The commands so far, once there are at least as many as expected or the time is up. */ - public List await(final int count, final Duration timeout) throws InterruptedException { - final Instant deadline = Instant.now().plus(timeout); - - while (commands.size() < count && Instant.now().isBefore(deadline)) { - Thread.sleep(POLL_MILLIS); - } - - return commands(); - } - - @Override - public Optional chooseFile(final String title) { - commands.add("choose-file:" + title); - return Optional.ofNullable(picked); - } - - @Override - public Optional saveFile(final String title, final String name) { - commands.add("save-file:" + title + ":" + name); - return Optional.ofNullable(picked); - } - - @Override - public void minimise() { - commands.add("minimise"); - } - - @Override - public void toggleMaximise() { - commands.add("maximise"); - } - - @Override - public void close() { - commands.add("close"); - } - - @Override - public void beginMove() { - commands.add("move/start"); - } - - @Override - public void endMove() { - commands.add("move/end"); - } - - @Override - public void beginResize(final Edge edge) { - commands.add("resize/start/" + edge.pathName()); - } - - @Override - public void endResize() { - commands.add("resize/end"); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/RoutesTest.java b/web/src/test/java/org/unigrid/janus/web/RoutesTest.java deleted file mode 100644 index e67892c5..00000000 --- a/web/src/test/java/org/unigrid/janus/web/RoutesTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.http.HttpResponse; -import net.jqwik.api.Example; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class RoutesTest extends ServedTest { - @Example - public void shouldServeTheStylesheetAsCss() throws Exception { - final HttpResponse response = admitted().get("/static/css/testing.css"); - - assertEquals(200, response.statusCode()); - assertTrue(response.headers().firstValue("content-type").orElse("").startsWith("text/css"), - response.headers().toString() - ); - assertTrue(response.body().contains("--accent"), response.body()); - } - - @Example - public void shouldFallThroughToThePageForUnknownPaths() throws Exception { - final Client client = admitted(); - - assertTrue(client.get("/").body().contains("

Janus

")); - assertTrue(client.get("/wallet").body().contains("

Janus

")); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/ServedTest.java b/web/src/test/java/org/unigrid/janus/web/ServedTest.java deleted file mode 100644 index aa0688d2..00000000 --- a/web/src/test/java/org/unigrid/janus/web/ServedTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.URI; -import net.jqwik.api.lifecycle.AfterTry; -import net.jqwik.api.lifecycle.BeforeTry; -import org.eclipse.jetty.server.Handler; - -/** Runs the interface on a real port for the length of one test. */ -public abstract class ServedTest { - private final SessionToken token = SessionToken.random(); - private final Templates templates = new Templates(false); - - private UiServer server; - private URI base; - - protected Handler routes() { - return Routes.create(templates, token, new Page("Janus")); - } - - @BeforeTry - public void serve() throws Exception { - server = new UiServer(routes()); - base = server.start(); - } - - @AfterTry - public void stop() throws Exception { - server.stop(); - } - - protected SessionToken token() { - return token; - } - - protected Templates templates() { - return templates; - } - - protected URI base() { - return base; - } - - /** A caller that has not proven anything yet. */ - protected Client anonymous() { - return new Client(base); - } - - /** A caller holding the session cookie, as the interface itself does. */ - protected Client admitted() throws Exception { - final Client client = anonymous(); - - client.get("/?" + SessionToken.PARAMETER + "=" + token.value()); - return client; - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/Status.java b/web/src/test/java/org/unigrid/janus/web/Status.java deleted file mode 100644 index eff834c2..00000000 --- a/web/src/test/java/org/unigrid/janus/web/Status.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import org.unigrid.janus.web.action.View; - -/** A block of a template rather than a whole page. */ -public record Status(String holder) implements View { - @Override - public String template() { - return "testing :: status"; - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/TemplatesTest.java b/web/src/test/java/org/unigrid/janus/web/TemplatesTest.java deleted file mode 100644 index 5a823320..00000000 --- a/web/src/test/java/org/unigrid/janus/web/TemplatesTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import net.jqwik.api.Example; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class TemplatesTest { - private final Templates templates = new Templates(false); - - @Example - public void shouldResolveTemplateFromClasspath() { - final String html = templates.render(new Page("Janus")); - - assertTrue(html.contains(""), html); - } - - @Example - public void shouldEvaluateExpressionsRatherThanEchoTheFile() { - final String html = templates.render(new Page("Unigrid")); - - assertTrue(html.contains("

Unigrid

"), html); - assertTrue(!html.contains("th:text"), "the th: attributes should be consumed during rendering"); - } - - @Example - public void shouldRenderOnlyTheNamedBlockOfATemplate() { - final String html = templates.render(new Status("Alice")); - - assertTrue(html.contains("unlocked by Alice"), html); - assertTrue(!html.contains(""), "a block should not carry the whole page: " + html); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/UiHandlerTest.java b/web/src/test/java/org/unigrid/janus/web/UiHandlerTest.java deleted file mode 100644 index c2d0fdc6..00000000 --- a/web/src/test/java/org/unigrid/janus/web/UiHandlerTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.util.concurrent.atomic.AtomicInteger; -import net.jqwik.api.Example; -import org.eclipse.jetty.server.Handler; -import org.unigrid.janus.web.action.Actions; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class UiHandlerTest extends ServedTest { - private final AtomicInteger visits = new AtomicInteger(); - - @Override - protected Handler routes() { - return Routes.create(templates(), token(), WindowControl.NONE, Actions.of(), - () -> new Page("Visit " + visits.incrementAndGet()) - ); - } - - /* What the page opens on can change while Janus runs, such as once a wallet has been chosen. */ - @Example - public void shouldWorkOutThePageAgainForEveryRequest() throws Exception { - final Client client = admitted(); - - assertTrue(client.get("/").body().contains("Visit 2")); - assertTrue(client.get("/").body().contains("Visit 3")); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/UiServerTest.java b/web/src/test/java/org/unigrid/janus/web/UiServerTest.java deleted file mode 100644 index 8242dba7..00000000 --- a/web/src/test/java/org/unigrid/janus/web/UiServerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.http.HttpResponse; -import net.jqwik.api.Example; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class UiServerTest extends ServedTest { - @Example - public void shouldServeTheRenderedPage() throws Exception { - final HttpResponse response = admitted().get("/"); - - assertEquals(200, response.statusCode()); - assertTrue(response.body().contains("

Janus

"), response.body()); - } - - @Example - public void shouldBindToLoopbackOnly() { - assertEquals("127.0.0.1", base().getHost()); - assertTrue(base().getPort() > 0, "an ephemeral port should have been assigned"); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/WindowHandlerTest.java b/web/src/test/java/org/unigrid/janus/web/WindowHandlerTest.java deleted file mode 100644 index fe4d36e6..00000000 --- a/web/src/test/java/org/unigrid/janus/web/WindowHandlerTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web; - -import java.net.http.HttpResponse; -import java.nio.file.Path; -import java.util.List; -import net.jqwik.api.Example; -import org.eclipse.jetty.server.Handler; -import org.unigrid.janus.web.action.Actions; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class WindowHandlerTest extends ServedTest { - private final RecordingWindow window = new RecordingWindow(); - - @Override - protected Handler routes() { - return Routes.create(templates(), token(), window, Actions.of(), new Page("Janus")); - } - - @Example - public void shouldForwardEachCommandToTheHost() throws Exception { - final Client client = admitted(); - - assertEquals(204, client.post("/window/minimise").statusCode()); - assertEquals(204, client.post("/window/maximise").statusCode()); - assertEquals(204, client.post("/window/close").statusCode()); - assertEquals(204, client.post("/window/move/start").statusCode()); - assertEquals(204, client.post("/window/move/end").statusCode()); - assertEquals(List.of("minimise", "maximise", "close", "move/start", "move/end"), window.commands()); - } - - @Example - public void shouldForwardEachResizableEdgeToTheHost() throws Exception { - final Client client = admitted(); - - assertEquals(204, client.post("/window/resize/start/left").statusCode()); - assertEquals(204, client.post("/window/resize/start/right").statusCode()); - assertEquals(204, client.post("/window/resize/start/bottom").statusCode()); - assertEquals(204, client.post("/window/resize/start/bottom-right").statusCode()); - assertEquals(204, client.post("/window/resize/end").statusCode()); - assertEquals(List.of( - "resize/start/left", "resize/start/right", "resize/start/bottom", "resize/start/bottom-right", - "resize/end" - ), window.commands()); - } - - @Example - public void shouldAnswerWithTheFileTheHostChose() throws Exception { - final Path wallet = Path.of("/mnt/backup/wallet.dat"); - - window.picking(wallet); - - final HttpResponse response = admitted().post("/window/choose-file?title=Pick+a+wallet"); - - assertEquals(200, response.statusCode()); - assertEquals(wallet.toString(), response.body()); - assertEquals(List.of("choose-file:Pick a wallet"), window.commands()); - } - - @Example - public void shouldAnswerWithNothingWhenTheDialogWasDismissed() throws Exception { - final HttpResponse response = admitted().post("/window/choose-file?title=Pick+a+wallet"); - - assertEquals(204, response.statusCode()); - assertEquals("", response.body()); - } - - @Example - public void shouldNotInventCommandsItDoesNotHave() throws Exception { - admitted().post("/window/selfdestruct"); - admitted().post("/window/resize/start/top"); - assertEquals(List.of(), window.commands()); - } - - @Example - public void shouldNotActForAnUnknownCaller() throws Exception { - assertEquals(403, anonymous().post("/window/close").statusCode()); - assertEquals(List.of(), window.commands()); - } - - @Example - public void shouldAnswerWithWhereTheHostWillSave() throws Exception { - final Path history = Path.of("/home/ann/history.csv"); - - window.picking(history); - - final HttpResponse response = admitted() - .post("/window/save-file?title=Save+the+history&name=history.csv"); - - assertEquals(200, response.statusCode()); - assertEquals(history.toString(), response.body()); - assertEquals(List.of("save-file:Save the history:history.csv"), window.commands()); - } - - @Example - public void shouldAnswerWithNothingWhenSavingWasCancelled() throws Exception { - assertEquals(204, admitted().post("/window/save-file?title=Save&name=a.csv").statusCode()); - } -} diff --git a/web/src/test/java/org/unigrid/janus/web/action/ActionsTest.java b/web/src/test/java/org/unigrid/janus/web/action/ActionsTest.java deleted file mode 100644 index 43ee5cd5..00000000 --- a/web/src/test/java/org/unigrid/janus/web/action/ActionsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - The Janus Wallet - Copyright © 2021-2026 Stiftelsen The Unigrid Foundation - - This program is free software: you can redistribute it and/or modify it under the terms of the - addended GNU Affero General Public License as published by the Free Software Foundation, version 3 - of the License (see COPYING and COPYING.addendum). - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received an addended copy of the GNU Affero General Public License with this program. - If not, see and . - */ - -package org.unigrid.janus.web.action; - -import net.jqwik.api.Example; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ActionsTest { - public static class Padlock { - @Action("lock") - public void onClickLock() { - } - } - - public static class Bolt { - @Action("lock") - public void onClickBolt() { - } - } - - public static class Wallet { - @Action("lock") - public void onClickLock() { - } - - @Action("unlock") - public void onClickUnlock() { - } - } - - @Example - public void shouldRefuseTwoMethodsClaimingTheSameAction() { - final IllegalStateException thrown = assertThrows(IllegalStateException.class, - () -> Actions.of(new Padlock(), new Bolt()) - ); - - assertTrue(thrown.getMessage().contains("lock"), thrown.getMessage()); - } - - @Example - public void shouldBindEachActionOnce() { - final Actions actions = Actions.of(new Wallet()); - - assertTrue(actions.knows("lock")); - assertTrue(actions.knows("unlock")); - } -} diff --git a/web/src/test/resources/logback-test.xml b/web/src/test/resources/logback-test.xml deleted file mode 100644 index 5eeb53e5..00000000 --- a/web/src/test/resources/logback-test.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n - - - - - - - - - - - diff --git a/web/src/test/resources/static/css/testing.css b/web/src/test/resources/static/css/testing.css deleted file mode 100644 index 26cf4151..00000000 --- a/web/src/test/resources/static/css/testing.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - --accent: #7c4dff; -} diff --git a/web/src/test/resources/templates/page.html b/web/src/test/resources/templates/page.html deleted file mode 100644 index 3d9778b8..00000000 --- a/web/src/test/resources/templates/page.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Janus - - -

Janus

- - diff --git a/web/src/test/resources/templates/testing.html b/web/src/test/resources/templates/testing.html deleted file mode 100644 index e15beeec..00000000 --- a/web/src/test/resources/templates/testing.html +++ /dev/null @@ -1,8 +0,0 @@ - - - -
-

Janus is unlocked by nobody

-
- -