more groundwork for the frontend
This commit is contained in:
parent
d0dadfea39
commit
afc1d2c764
|
@ -103,6 +103,65 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a6e7ebd44d0047fd48206c83c5cd3214acc7b9d87f001da170145c47ef7d12"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_escape",
|
||||
"askama_shared",
|
||||
"mime 0.3.16",
|
||||
"mime_guess",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d7169690c4f56343dcd821ab834972a22570a2662a19a84fd7775d5e1c3881"
|
||||
dependencies = [
|
||||
"askama_shared",
|
||||
"proc-macro2 1.0.21",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_escape"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb"
|
||||
|
||||
[[package]]
|
||||
name = "askama_rocket"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40428c13bd027adfd481391729b85bf9961b59e5b21bb55e80f603d19d054583"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"rocket",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_shared"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62fc272363345c8cdc030e4c259d9d028237f8b057dc9bb327772a257bde6bb5"
|
||||
dependencies = [
|
||||
"askama_escape",
|
||||
"humansize",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"percent-encoding 2.1.0",
|
||||
"proc-macro2 1.0.21",
|
||||
"quote 1.0.7",
|
||||
"serde",
|
||||
"syn 1.0.40",
|
||||
"toml 0.5.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
|
@ -742,6 +801,12 @@ version = "1.3.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.10.16"
|
||||
|
@ -974,6 +1039,8 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
|||
name = "mi"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_rocket",
|
||||
"chrono",
|
||||
"color-eyre",
|
||||
"diesel",
|
||||
|
@ -1016,6 +1083,16 @@ version = "0.3.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
|
||||
dependencies = [
|
||||
"mime 0.3.16",
|
||||
"unicase 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.1"
|
||||
|
@ -1079,6 +1156,16 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"version_check 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "4.0.15"
|
||||
|
@ -1539,7 +1626,7 @@ dependencies = [
|
|||
"rocket_http",
|
||||
"state",
|
||||
"time 0.1.44",
|
||||
"toml",
|
||||
"toml 0.4.10",
|
||||
"version_check 0.9.2",
|
||||
"yansi",
|
||||
]
|
||||
|
@ -2053,6 +2140,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.19"
|
||||
|
|
|
@ -7,6 +7,7 @@ edition = "2018"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
askama_rocket = "0.10"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
color-eyre = "0.5"
|
||||
futures-io = "0.3"
|
||||
|
@ -31,6 +32,10 @@ twapi-ureq = "0.1.5"
|
|||
ureq = { version = "1", features = ["json", "charset"] }
|
||||
url = "2"
|
||||
|
||||
[dependencies.askama]
|
||||
version = "0.10"
|
||||
features = [ "with-rocket" ]
|
||||
|
||||
[dependencies.diesel]
|
||||
version = "1"
|
||||
features = ["sqlite", "r2d2", "chrono"]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
address = "0.0.0.0"
|
||||
switchcounter_webhook = "..."
|
||||
discord_webhook = ""
|
||||
asset_path = "../static"
|
||||
|
||||
# I don't know how to generate these without using Go, sorry. These are hex-encoded
|
||||
# ED25519 public/private keys.
|
||||
|
|
|
@ -6,6 +6,7 @@ let
|
|||
rustc = rust;
|
||||
cargo = rust;
|
||||
};
|
||||
gruvbox = pkgs.callPackage sources.gruvbox-css { };
|
||||
src = builtins.filterSource
|
||||
(path: type: type != "directory" || builtins.baseNameOf path != "target")
|
||||
./.;
|
||||
|
@ -13,6 +14,7 @@ in naersk.buildPackage {
|
|||
name = "mi_backend";
|
||||
inherit src;
|
||||
buildInputs = with pkgs; [ openssl pkg-config sqlite libsodium ];
|
||||
GRUVBOX_CSS = "${gruvbox}/gruvbox.css";
|
||||
SODIUM_USE_PKG_CONFIG = "1";
|
||||
SODIUM_SHARED = "1";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
use askama::Template;
|
||||
use rocket::{fairing::AdHoc, response::content::Css, Request};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "app.html")]
|
||||
struct App {
|
||||
title: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn frontend() -> App {
|
||||
App {
|
||||
title: "Mi".to_string(),
|
||||
message: "Loading...".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "notfound.html")]
|
||||
struct NotFound {
|
||||
title: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
fn not_found(req: &Request) -> NotFound {
|
||||
NotFound {
|
||||
title: "Not found".to_string(),
|
||||
message: format!("{} not found", req.uri()),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/static/gruvbox.css")]
|
||||
fn gruvbox() -> Css<String> {
|
||||
Css(include_str!(env!("GRUVBOX_CSS")).to_string())
|
||||
}
|
||||
|
||||
pub fn fairing() -> AdHoc {
|
||||
AdHoc::on_attach("frontend integration", |rocket| {
|
||||
Ok(rocket
|
||||
.register(catchers![not_found])
|
||||
.mount("/", routes![frontend, gruvbox]))
|
||||
})
|
||||
}
|
|
@ -19,6 +19,7 @@ pub const APPLICATION_NAME: &str = concat!(
|
|||
);
|
||||
|
||||
pub mod api;
|
||||
pub mod frontend;
|
||||
pub mod models;
|
||||
pub mod paseto;
|
||||
pub mod rocket_trace;
|
||||
|
|
|
@ -11,7 +11,7 @@ use rocket_contrib::helmet::SpaceHelmet;
|
|||
use rocket_cors::{AllowedHeaders, AllowedOrigins};
|
||||
use rocket_prometheus::PrometheusMetrics;
|
||||
|
||||
use ::mi::{api, paseto, rocket_trace::*, web::*, MainDatabase, APPLICATION_NAME};
|
||||
use ::mi::{api, frontend, paseto, rocket_trace::*, web::*, MainDatabase, APPLICATION_NAME};
|
||||
|
||||
#[get("/.within/botinfo")]
|
||||
fn botinfo() -> &'static str {
|
||||
|
@ -58,8 +58,10 @@ fn main() -> Result<()> {
|
|||
rocket::ignite()
|
||||
.attach(prometheus.clone())
|
||||
.attach(cors)
|
||||
.attach(MainDatabase::fairing())
|
||||
.attach(SpaceHelmet::default())
|
||||
.attach(static_files())
|
||||
.attach(frontend::fairing())
|
||||
.attach(MainDatabase::fairing())
|
||||
.attach(RequestId {})
|
||||
.attach(paseto::ed25519_keypair())
|
||||
.attach(DiscordWebhook::fairing())
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use rocket::fairing::{Fairing, Info, Kind};
|
||||
use rocket::fairing::{AdHoc, Fairing, Info, Kind};
|
||||
use rocket::http::Header;
|
||||
use rocket::{Data, Request, Response};
|
||||
use rocket_contrib::serve::StaticFiles;
|
||||
use rusty_ulid::generate_ulid_string;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RequestId;
|
||||
|
||||
impl Fairing for RequestId {
|
||||
|
@ -32,3 +34,11 @@ impl Fairing for RequestId {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn static_files() -> AdHoc {
|
||||
AdHoc::on_attach("Static fileserver", |rocket| {
|
||||
let asset_path = rocket.config().get_string("asset_path").unwrap();
|
||||
|
||||
Ok(rocket.mount("/static", StaticFiles::from(asset_path)))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/static/gruvbox.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png"/>
|
||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon"/>
|
||||
</head>
|
||||
<body id="top">
|
||||
<main>
|
||||
<div id="app">{{ message }}</div>
|
||||
<script src="/static/elm.js"></script>
|
||||
<script>
|
||||
var app = Elm.Signup.init({
|
||||
node: document.getElementById("app")
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/static/gruvbox.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png"/>
|
||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon"/>
|
||||
</head>
|
||||
<body id="top">
|
||||
<main>
|
||||
<nav class="nav">
|
||||
<a href="/">Mi</a>
|
||||
</nav>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
<a href="/">Go home</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
|
@ -1,3 +1,2 @@
|
|||
index.html
|
||||
index.js
|
||||
elm-stuff
|
||||
elm-stuff
|
||||
elm.js
|
|
@ -15,6 +15,11 @@
|
|||
version = "1.0.2";
|
||||
};
|
||||
|
||||
"rtfeldman/elm-css" = {
|
||||
sha256 = "0nxiyxyw3kw55whkpwhrcgc0dr6a8zlm2nqvsaqdw6mzkykg0ba6";
|
||||
version = "16.1.0";
|
||||
};
|
||||
|
||||
"elm/core" = {
|
||||
sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf";
|
||||
version = "1.0.5";
|
||||
|
@ -50,6 +55,11 @@
|
|||
version = "1.0.5";
|
||||
};
|
||||
|
||||
"rtfeldman/elm-hex" = {
|
||||
sha256 = "1y0aa16asvwdqmgbskh5iba6psp43lkcjjw9mgzj3gsrg33lp00d";
|
||||
version = "1.0.0";
|
||||
};
|
||||
|
||||
"elm/parser" = {
|
||||
sha256 = "0a3cxrvbm7mwg9ykynhp7vjid58zsw03r63qxipxp3z09qks7512";
|
||||
version = "1.1.0";
|
||||
|
|
|
@ -13,13 +13,15 @@
|
|||
"elm/json": "1.1.3",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"rtfeldman/elm-css": "16.1.0",
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.1.3"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/bytes": "1.0.8",
|
||||
"elm/file": "1.0.5",
|
||||
"elm/parser": "1.1.0",
|
||||
"elm/virtual-dom": "1.0.2"
|
||||
"elm/virtual-dom": "1.0.2",
|
||||
"rtfeldman/elm-hex": "1.0.0"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#! nix-shell -i bash -p elmPackages.elm
|
||||
|
||||
elm make ./src/Main.elm --output elm.js
|
|
@ -6,6 +6,7 @@ import Html.Attributes exposing (autofocus, class, value)
|
|||
import Html.Events exposing (onClick, onInput)
|
||||
import Mi
|
||||
import Mi.Switch
|
||||
import Mi.WebMention
|
||||
|
||||
|
||||
-- We added a new AddTodo message type.
|
||||
|
@ -48,7 +49,6 @@ update msg model =
|
|||
UpdateText newText ->
|
||||
{ model | text = newText }
|
||||
|
||||
-- We append the model.text value to the end of our list of todo strings.
|
||||
AddTodo ->
|
||||
{ model | text = "", todos = model.todos ++ [ model.text ] }
|
||||
|
||||
|
@ -76,4 +76,4 @@ main =
|
|||
{ init = { text = "", todos = [] }
|
||||
, view = view
|
||||
, update = update
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
module Mi.Switch exposing (..)
|
||||
module Mi.Switch exposing (Switch, decoder, frontURL, idURL, listURL, switchURL)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Iso8601
|
||||
import Json.Decode exposing (Decoder, field, int, map5, nullable, string)
|
||||
import Time exposing (..)
|
||||
import Time exposing (Posix)
|
||||
import Url.Builder as UB
|
||||
|
||||
|
||||
type alias Switch =
|
||||
{ id : String
|
||||
, who : String
|
||||
|
@ -36,21 +35,21 @@ switchURL =
|
|||
idURL : String -> String
|
||||
idURL id =
|
||||
UB.absolute
|
||||
[ "api","switches", "id", id ]
|
||||
[ "api", "switches", "id", id ]
|
||||
[]
|
||||
|
||||
|
||||
frontURL : String
|
||||
frontURL =
|
||||
UB.absolute
|
||||
["api", "switches", "current" ]
|
||||
[ "api", "switches", "current" ]
|
||||
[]
|
||||
|
||||
|
||||
listURL : Int -> Int -> String
|
||||
listURL limit page =
|
||||
UB.absolute
|
||||
["api", "switches", "" ]
|
||||
[ "api", "switches", "" ]
|
||||
[ UB.int "limit" limit
|
||||
, UB.int "page" page
|
||||
]
|
||||
]
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
module Mi.WebMention exposing (decoder, idURL, listURL)
|
||||
|
||||
import Json.Decode exposing (Decoder, field, map3, string)
|
||||
import Url.Builder as UB
|
||||
|
||||
|
||||
type alias WebMention =
|
||||
{ id : String
|
||||
, source_url : String
|
||||
, target_url : String
|
||||
}
|
||||
|
||||
|
||||
decoder : Decoder WebMention
|
||||
decoder =
|
||||
map3 WebMention
|
||||
(field "id" string)
|
||||
(field "source_url" string)
|
||||
(field "target_url" string)
|
||||
|
||||
|
||||
idURL : String -> String
|
||||
idURL id =
|
||||
UB.absolute
|
||||
[ "api", "webmention", id ]
|
||||
[]
|
||||
|
||||
|
||||
listURL : Int -> Int -> String
|
||||
listURL limit page =
|
||||
UB.absolute
|
||||
[ "api", "webmention" ]
|
||||
[ UB.int "limit" limit
|
||||
, UB.int "page" page
|
||||
]
|
|
@ -0,0 +1,115 @@
|
|||
module Signup exposing (main)
|
||||
|
||||
import Browser
|
||||
import Css exposing (..)
|
||||
import Html exposing (Attribute, Html, button, div, h1, input, text)
|
||||
import Html.Attributes exposing (id, style, type_)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
|
||||
|
||||
type alias User =
|
||||
{ name : String
|
||||
, email : String
|
||||
, password : String
|
||||
, loggedIn : Bool
|
||||
}
|
||||
|
||||
|
||||
initialModel : User
|
||||
initialModel =
|
||||
{ name = ""
|
||||
, email = ""
|
||||
, password = ""
|
||||
, loggedIn = False
|
||||
}
|
||||
|
||||
|
||||
view : User -> Html Msg
|
||||
view user =
|
||||
div [ style "margin" "auto" ]
|
||||
[ h1 [ style "padding-left" "3cm" ] [ text "Sign up" ]
|
||||
, Html.form formStyle
|
||||
[ div []
|
||||
[ text "Name"
|
||||
, input ([ id "name", type_ "text", onInput SaveName ] ++ inputStyle) []
|
||||
]
|
||||
, div []
|
||||
[ text "Email"
|
||||
, input ([ id "email", type_ "email", onInput SaveEmail ] ++ inputStyle) []
|
||||
]
|
||||
, div []
|
||||
[ text "Password"
|
||||
, input ([ id "password", type_ "password", onInput SavePassword ] ++ inputStyle) []
|
||||
]
|
||||
, div []
|
||||
[ button
|
||||
([ type_ "submit", onClick Signup ] ++ buttonStyle)
|
||||
[ text "Create my account" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
formStyle : List (Attribute msg)
|
||||
formStyle =
|
||||
[ style "border-radius" "5px"
|
||||
, style "background-color" "#f2f2f2"
|
||||
, style "padding" "20px"
|
||||
, style "width" "300px"
|
||||
]
|
||||
|
||||
|
||||
inputStyle : List (Attribute msg)
|
||||
inputStyle =
|
||||
[ style "display" "block"
|
||||
, style "width" "260px"
|
||||
, style "padding" "12px 20px"
|
||||
, style "margin" "8px 0"
|
||||
, style "border" "none"
|
||||
, style "border-radius" "4px"
|
||||
]
|
||||
|
||||
|
||||
buttonStyle : List (Attribute msg)
|
||||
buttonStyle =
|
||||
[ style "width" "300px"
|
||||
, style "background-color" "#397cd5"
|
||||
, style "color" "white"
|
||||
, style "padding" "14px 20px"
|
||||
, style "margin-top" "10px"
|
||||
, style "border" "none"
|
||||
, style "border-radius" "4px"
|
||||
, style "font-size" "16px"
|
||||
]
|
||||
|
||||
|
||||
type Msg
|
||||
= SaveName String
|
||||
| SaveEmail String
|
||||
| SavePassword String
|
||||
| Signup
|
||||
|
||||
|
||||
update : Msg -> User -> User
|
||||
update message user =
|
||||
case message of
|
||||
SaveName name ->
|
||||
{ user | name = name }
|
||||
|
||||
SaveEmail email ->
|
||||
{ user | email = email }
|
||||
|
||||
SavePassword password ->
|
||||
{ user | password = password }
|
||||
|
||||
Signup ->
|
||||
{ user | loggedIn = True }
|
||||
|
||||
|
||||
main : Program () User Msg
|
||||
main =
|
||||
Browser.sandbox
|
||||
{ init = initialModel
|
||||
, view = view
|
||||
, update = update
|
||||
}
|
|
@ -3,6 +3,7 @@ let
|
|||
pkgs =
|
||||
import sources.nixpkgs { overlays = [ (import sources.nixpkgs-mozilla) ]; };
|
||||
rust = import ./nix/rust.nix { };
|
||||
gruvbox = pkgs.callPackage sources.gruvbox-css { };
|
||||
in pkgs.mkShell rec {
|
||||
buildInputs = with pkgs; [
|
||||
# rust
|
||||
|
@ -26,6 +27,8 @@ in pkgs.mkShell rec {
|
|||
bashInteractive
|
||||
];
|
||||
|
||||
GRUVBOX_CSS = "${gruvbox}/gruvbox.css";
|
||||
|
||||
DATABASE_URL = "./mi.db";
|
||||
ROCKET_DATABASES = ''{ main_data = { url = "${DATABASE_URL}" } }'';
|
||||
RUST_LOG = "info";
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
elm.js
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
|
@ -0,0 +1 @@
|
|||
../frontend/app.html
|
Loading…
Reference in New Issue