Martin Kunz 4 years ago
commit
a179d1000e

+ 189 - 0
.gitignore

@@ -0,0 +1,189 @@
+
+# Created by https://www.gitignore.io/api/osx,java,linux,windows,intellij,maven
+# Edit at https://www.gitignore.io/?templates=osx,java,linux,windows,intellij,maven
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+### OSX ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.gitignore.io/api/osx,java,linux,windows,intellij,maven

+ 109 - 0
pom.xml

@@ -0,0 +1,109 @@
+<?xml version="1.0"?>
+<project
+		xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>at.acdp.ur</groupId>
+	<artifactId>uraxisdaemon</artifactId>
+	<version>1.0-SNAPSHOT</version>
+	<name>uraxisdaemon</name>
+	<packaging>jar</packaging>
+	<properties>
+		<undertow.version>2.0.15.Final</undertow.version>
+	</properties>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>com.github.spotbugs</groupId>
+				<artifactId>spotbugs-maven-plugin</artifactId>
+				<version>3.1.12</version>
+				<dependencies>
+					<!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
+					<dependency>
+						<groupId>com.github.spotbugs</groupId>
+						<artifactId>spotbugs</artifactId>
+						<version>3.1.12</version>
+					</dependency>
+				</dependencies>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<version>3.8.0</version>
+				<configuration>
+					<source>11</source>
+					<target>11</target>
+					<excludes>
+						<exclude>module-info.java</exclude>
+					</excludes>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-shade-plugin</artifactId>
+				<version>3.2.1</version>
+				<executions>
+					<execution>
+						<phase>package</phase>
+						<goals>
+							<goal>shade</goal>
+						</goals>
+						<configuration>
+							<minimizeJar>true</minimizeJar>
+							<filters>
+								<filter>
+									<artifact>*:*</artifact>
+									<excludes>
+										<exclude>META-INF/*.SF</exclude>
+										<exclude>META-INF/*.DSA</exclude>
+										<exclude>META-INF/*.RSA</exclude>
+									</excludes>
+								</filter>
+							</filters>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<version>3.1.1</version>
+				<configuration>
+					<archive>
+						<manifest>
+							<addClasspath>true</addClasspath>
+							<mainClass>at.acdp.urweb.Main</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+	<dependencies>
+        <dependency>
+            <groupId>com.sparkjava</groupId>
+            <artifactId>spark-core</artifactId>
+            <version>2.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.26</version>
+        </dependency>
+		<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-api</artifactId>
+			<version>5.3.1</version>
+			<scope>test</scope>
+		</dependency>
+		<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-classic</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+	</dependencies>
+</project>

File diff suppressed because it is too large
+ 12601 - 0
src/main/java/at/acdp/urweb/CommandLine.java


+ 24 - 0
src/main/java/at/acdp/urweb/Main.java

@@ -0,0 +1,24 @@
+package at.acdp.urweb;
+
+import at.acdp.urweb.web.WebServer;
+import org.slf4j.LoggerFactory;
+
+public class Main {
+    private final static org.slf4j.Logger logger = LoggerFactory.getLogger(Main.class);
+
+    public static void main(String[] args) {
+        Params app = null;
+        try {
+            app = picocli.CommandLine.populateCommand(new Params(), args);
+        } catch (Exception e) {
+            logger.error("failed.", e);
+            picocli.CommandLine.usage(new Params(), System.out);
+            System.exit(1);
+        }
+        try {
+            new WebServer(app).start();
+        } catch (Exception e) {
+            logger.error("Server exited", e);
+        }
+    }
+}

+ 28 - 0
src/main/java/at/acdp/urweb/Params.java

@@ -0,0 +1,28 @@
+package at.acdp.urweb;
+
+public class Params {
+    @picocli.CommandLine.Option(names = { "-p", "--port" }, description = "HTTP Server port", required = true)
+    public int port = 8080;
+
+    @picocli.CommandLine.Option(names = { "-w", "--webroot" }, description = "Use webroot from filesystem", defaultValue = "")
+    public String webroot = "";
+
+    @picocli.CommandLine.Option(names = { "-rip", "--robotip" }, description = "Robot ip address", defaultValue = "")
+    public String robotIP = "";
+
+    @picocli.CommandLine.Option(names = { "-rp", "--robotport" }, description = "Robot tcp port", defaultValue = "30001")
+    public int robotPort = 30001;
+
+    @picocli.CommandLine.Option(names = { "-rt", "--rtport" }, description = "rtde tcp port", defaultValue = "30004")
+    public int rtPort;
+
+    @picocli.CommandLine.Option(names = {"--sshUsername"}, description = "ssh username", defaultValue = "root")
+    public String sshUsername;
+
+    @picocli.CommandLine.Option(names = {"--sshPassword"}, description = "ssh password", defaultValue = "easybot")
+    public String sshPassword;
+
+    @picocli.CommandLine.Option(names = {"--sshPort"}, description = "ssh port", defaultValue = "22")
+    public String sshPort;
+}
+

+ 107 - 0
src/main/java/at/acdp/urweb/web/WebServer.java

@@ -0,0 +1,107 @@
+package at.acdp.urweb.web;
+
+import at.acdp.urweb.Params;
+import at.acdp.urweb.RobotCommand;
+import at.acdp.urweb.URBot;
+import at.acdp.urweb.rt.GetRobotRealtimeData;
+import at.acdp.urweb.sclient.URLog;
+import com.eclipsesource.json.JsonArray;
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.xfer.FileSystemFile;
+import org.slf4j.LoggerFactory;
+
+import static spark.Spark.*;
+
+public class WebServer {
+    private final static org.slf4j.Logger logger = LoggerFactory.getLogger(WebServer.class);
+    private URBot urbot;
+    private final Params params;
+    private GetRobotRealtimeData rtbot;
+
+    public WebServer(Params params) {
+        this.params = params;
+    }
+
+    public void start() {
+        this.urbot=new URBot(params.robotIP, params.robotPort);
+        this.urbot.start();
+        // this.rtbot=new GetRobotRealtimeData(params.robotIP, params.rtPort);
+        // this.rtbot.start();
+        port(params.port);
+        if (!params.webroot.isEmpty())
+            staticFileLocation(params.webroot);
+        else
+            staticFiles.location("/webroot");
+
+        post("/cmd", (req, res) -> {
+            var cmd=req.queryParams("script");
+            if(cmd==null)
+                cmd=req.body();
+            RobotCommand rc=new RobotCommand(cmd);
+            rc.cpeeCallback = req.headers("CPEE-CALLBACK");
+            rc.cpeeCallbackId = req.headers("CPEE-CALLBACK-ID");
+            rc.cpeeInstanceURL = req.headers("CPEE-INSTANCE-URL");
+            if (Boolean.valueOf(req.queryParams("callback"))) {
+                res.header("CPEE-CALLBACK", "true");
+                rc.doCpeeCallback = true;
+            }
+            urbot.sendCmd(rc);
+            return "";
+        });
+
+        post("/freedrive",  (req, res) -> {
+                urbot.sendFreedrive(1);
+                return "";
+        });
+        post("/digital/:which", (req, res) -> {
+            int which=Integer.parseInt(req.params("which"));
+            boolean val = Boolean.valueOf(new String(req.raw().getInputStream().readAllBytes()));
+            urbot.setDigital(which, val);
+            return "";
+        });
+        get("/digital/:which", (req, res) -> {
+            int which=Integer.parseInt(req.params("which"));
+            boolean d=urbot.getDigital(which);
+            res.body(String.valueOf(d));
+            return "";
+        });
+        get("/log/:from",  (req, res) -> {
+            int from=Integer.parseInt(req.params("from"));
+            from=Integer.max(0,from);
+            var r = URLog.get(from);
+            return r.toJSON();
+        });
+        get("/cmdq",  (req, res) -> {
+            JsonArray jsa=new JsonArray();
+            for(var c:urbot.getCmdq()) {
+                jsa.add(c.toJSON());
+            }
+            return jsa.toString();
+        });
+        get("/running",  (req, res) -> {
+            var rc=urbot.getRunningScript();
+            if(rc.isPresent()) {
+                return rc.get().toJSON();
+            }
+            return "[{}]";
+        });
+        get("/files/:path",  (req, res) -> {
+            String path=req.params("from");
+            SSHClient ssh = new SSHClient();
+            // ssh.useCompression(); // Can lead to significant speedup (needs JZlib in classpath)
+            ssh.loadKnownHosts();
+            ssh.connect(params.robotIP, params.robotPort);
+            try {
+                ssh.authPublickey(System.getProperty(params.sshUsername));
+                ssh.newSCPFileTransfer().download(path, new FileSystemFile("C:\\tmp\\"));
+
+            }catch (Exception e) {
+                logger.warn("",e);
+
+            } finally{
+                ssh.disconnect();
+            }
+            return "[{}]";
+        });
+    }
+}

+ 28 - 0
src/main/resources/META-INF/LICENSE

@@ -0,0 +1,28 @@
+Example:
+Copyright (c) <year>, <copyright holder>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. All advertising materials mentioning features or use of this software
+   must display the following acknowledgement:
+   This product includes software developed by the <organization>.
+4. Neither the name of the <organization> nor the
+   names of its contributors may be used to endorse or promote products
+   derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 15 - 0
src/main/resources/logback.xml

@@ -0,0 +1,15 @@
+<configuration debug="true" >
+
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <layout class="ch.qos.logback.classic.PatternLayout">
+            <Pattern>
+                %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
+            </Pattern>
+        </layout>
+    </appender>
+
+    <root level="info">
+        <appender-ref ref="CONSOLE"/>
+    </root>
+
+</configuration>

+ 33 - 0
src/main/resources/webroot/css/style.css

@@ -0,0 +1,33 @@
+Edit in JSFiddle
+Result
+HTML
+JavaScript
+CSS
+html, body, #editor {
+    margin: 0;
+    height: 100%;
+    font-family: 'Helvetica Neue', Arial, sans-serif;
+    color: #333;
+}
+
+textarea, #editor div {
+    display: inline-block;
+    vertical-align: top;
+    box-sizing: border-box;
+    padding: 0 20px;
+}
+
+textarea {
+    border: none;
+    border-right: 1px solid #ccc;
+    resize: none;
+    outline: none;
+    background-color: #f6f6f6;
+    font-size: 14px;
+    font-family: 'Monaco', courier, monospace;
+    padding: 20px;
+}
+
+code {
+    color: #f66;
+}

+ 225 - 0
src/main/resources/webroot/index.html

@@ -0,0 +1,225 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>Vue.js markdown editor example</title>
+    <link rel="stylesheet" href="css/style.css">
+    <script src="js/util.js"></script>
+    <script src="js/moment.js"></script>
+    <script src="js/vue.js"></script>
+    <style>
+        table {
+            display: inline-table;
+            width:50%;
+        }
+        td {
+            max-width: 100px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+    </style>
+</head>
+<body>
+<div id="app">
+    <textarea v-model="input" cols="80" rows="20"></textarea>
+    <button v-on:click="sendCommand">sendCommand</button>
+    <button v-on:click="sendFreeDrive">sendFreeDrive</button>
+    <button v-on:click="saveWP">saveWP</button>
+
+    <table border="1">
+        <tr v-for="x in waypoints">
+            <td>{{x}}</td>
+            <td>{{x}}</td>
+            <td>{{x}}</td>
+        </tr>
+
+    </table>
+
+
+    Running: {{ $data.running }}
+    <table border="1">
+        <tr v-for="x in cmdq">
+            <td>{{x.id}}</td>
+            <td>{{x.cmd}}</td>
+            <td>{{x.cpeeCallback}}</td>
+            <td>
+            </td>
+        </tr>
+    </table>
+
+
+    <table border="1">
+        <tr v-for="x in cData">
+            <td>{{x.type}}</td>
+            <td>{{x.ts}}</td>
+            <td>
+                <pre>{{JSON.stringify(x.entry, null, 2)}}</pre>
+            </td>
+        </tr>
+    </table>
+    <ul id="loglist">
+        <li v-for="(item, index) in log.slice().reverse()">
+            {{ ts2txt(item.ts) }} - {{ item.entry.message }} -
+            <pre>{{ JSON.stringify(item.entry) }}</pre>
+        </li>
+    </ul>
+</div>
+
+<script type="application/javascript">
+    new Vue({
+        el: '#app',
+        data: {
+            input: '// rde.writeCmd("set_digital_out(2,True)");\n' +
+                '// rde.writeCmd("movej([-1.95,-1.58,-1.16,-1.15,-1.55,1.25], a=1.0, v=0.1)");\n' +
+                '//rde.writeCmd("freedrive_mode()");\n' +
+                'def asdf():\n' +
+                ' set_digital_out(3, True) \n' +
+                ' set_digital_out(4, True) \n' +
+                'end',
+            log: [],
+            lastID: -1,
+            doBits: [],
+            cData: {},
+            cmdq: [],
+            curPos: {},
+            waypoints: []
+        },
+        created: function () {
+        setInterval(this.update, 210);
+        setInterval(this.updateQ, 220);
+        setInterval(this.updateCurrent, 230);
+
+        },
+        watch: {
+            doBits: (newValue, oldValue) => {
+                let diff1 = newValue.filter(x => !oldValue.includes(x));
+                let diff2 = oldValue.filter(x => !newValue.includes(x));
+                for (const x of diff1) {
+                    fetch('/digital/' + x, {method: "POST", body: 'True'})
+                        .then(function (response) {
+                            return response;
+                        });
+                }
+                for (const x of diff2) {
+                    fetch('/digital/' + x, {method: "POST", body: 'False'})
+                        .then(function (response) {
+                            return response;
+                        });
+                }
+            }
+        },
+        methods: {
+            sendCommand: function (event) {
+                var params  = new URLSearchParams();
+                params.append('script', this.input);
+                fetch('/cmd', {method: "POST", body: params})
+                    .then(function (response) {
+                        return response;
+                    });
+            },
+            sendFreeDrive: function (event) {
+                var params  = new URLSearchParams();
+                fetch('/freedrive', {method: "POST"})
+                    .then(function (response) {
+                        return response;
+                    });
+            },
+            saveWP: function (event) {
+                this.$data.waypoints.push_back(event)
+            },
+            ts2txt: function (ts) {
+                return moment(ts).format('YYYY-MM-DD hh:mm ss.SSS ')
+            },
+            handlePackage: function (package) {
+                Vue.set(this.$data.cData, package.type, package)
+                switch (package.type) {
+                    case 'Message':
+                        break;
+                    case 'MasterBoardData':
+                        let bits = package.entry.digitalOutputBits;
+                        for (let i = 0; i < 8; i++) {
+                            if (bits & (1 << i)) {
+                                this.$data.doBits.push('' + (i + 1));
+                            }
+                        }
+                        break;
+                    case 'ModeData':
+                        break;
+                    case 'ToolData':
+                        break;
+                    case 'ToolCommInfo':
+                        break;
+                    case 'JointDataList':
+                        break;
+                    case 'CartesianInfo':
+                        this.$data.curPos = package.entry;
+                        break;
+                    case 'ForceModeData':
+                        break;
+                    case 'AdditionalInfo':
+                        break;
+                    case 'KinematicsInfo':
+                        break;
+                    case 'ConfigurationData':
+                        break;
+                    case 'ProgramStateMessage':
+                        break;
+                    case 'ModbusInfoMessage':
+                        break;
+                    case 'RobotMessageError':
+                        break;
+                    default:
+                        console.log('unknown package:' + package.type);
+                }
+            },
+            updateCurrent: function (event) {
+                fetch('/running', {method: "GET"})
+                    .then(handleErrors)
+                    .then((response) => {
+                        return response.json();
+                    })
+                    .then((myJson) => {
+                        this.$data.running = myJson;
+                    })
+                    .catch(error => {
+                        console.log(error)
+                    })},
+            updateQ: function (event) {
+                fetch('/cmdq', {method: "GET"})
+                    .then(handleErrors)
+                    .then((response) => {
+                        return response.json();
+                    })
+                    .then((myJson) => {
+                        this.$data.cmdq = myJson;
+                    })
+                    .catch(error => {
+                        console.log(error)
+                    })},
+            update: function (event) {
+                fetch('/log/' + this.lastID, {method: "GET"})
+                    .then(handleErrors)
+                    .then((response) => {
+                        return response.json();
+                    })
+                    .then((myJson) => {
+                        for (const entry of myJson.entries) {
+                            this.handlePackage(entry);
+                        }
+                        this.$data.log = this.$data.log.concat(myJson.entries);
+                        this.$data.lastID = myJson.lastID;
+                        let len = this.$data.log.length;
+                        this.$data.log.splice(0, len - 200);
+                    })
+                    .catch(error => {
+                        this.$data.lastID = 0;
+                        this.$data.log = [];
+                        console.log(error)
+                    });
+            }
+        }
+    })
+</script>
+</body>
+</html>

File diff suppressed because it is too large
+ 4602 - 0
src/main/resources/webroot/js/moment.js


File diff suppressed because it is too large
+ 1 - 0
src/main/resources/webroot/js/moment.min.js


+ 6 - 0
src/main/resources/webroot/js/util.js

@@ -0,0 +1,6 @@
+function handleErrors(response) {
+    if (!response.ok) {
+        throw Error(response.statusText);
+    }
+    return response;
+}

File diff suppressed because it is too large
+ 11907 - 0
src/main/resources/webroot/js/vue.js


File diff suppressed because it is too large
+ 6 - 0
src/main/resources/webroot/js/vue.min.js