Browse Source

mad now supports >= 19.1

Namdak Tonpa 6 years ago
parent
commit
484403bd92
40 changed files with 511 additions and 164 deletions
  1. 1 1
      include/mad.hrl
  2. 5 1
      index.html
  3. BIN
      mad
  4. 3 2
      priv/mqtt/priv/static/index.htm
  5. 1 0
      priv/mqtt/priv/static/login.htm
  6. 0 0
      priv/mqtt/priv/static/n2o/bert.js
  7. 35 0
      priv/mqtt/priv/static/n2o/bullet.js
  8. 0 0
      priv/mqtt/priv/static/n2o/ftp.js
  9. 6 6
      priv/mqtt/priv/static/n2o/mq.js
  10. 0 0
      priv/mqtt/priv/static/n2o/n2o.js
  11. 0 0
      priv/mqtt/priv/static/n2o/nitrogen.js
  12. 0 0
      priv/mqtt/priv/static/n2o/utf8.js
  13. 1 1
      priv/mqtt/priv/static/synrc.css
  14. 1 0
      priv/mqtt/rebar.config
  15. 2 2
      priv/mqtt/src/index.erl
  16. 1 1
      priv/mqtt/src/sample.app.src
  17. 2 2
      priv/mqtt/src/sample.erl
  18. 2 1
      priv/mqtt/sys.config
  19. 0 3
      priv/web/apps/rebar.config
  20. 0 21
      priv/web/apps/sample/priv/static/spa/index.htm
  21. 0 3
      priv/web/apps/sample/priv/static/synrc.css
  22. 0 20
      priv/web/apps/sample/priv/templates/index.html
  23. 0 7
      priv/web/apps/sample/rebar.config
  24. 0 15
      priv/web/apps/sample/src/index.erl
  25. 0 20
      priv/web/apps/sample/src/routes.erl
  26. 0 7
      priv/web/apps/sample/src/sample.app.src
  27. 0 26
      priv/web/apps/sample/src/sample.erl
  28. 41 0
      priv/web/priv/static/index.htm
  29. 38 0
      priv/web/priv/static/login.htm
  30. 202 0
      priv/web/priv/static/synrc.css
  31. 1 0
      priv/web/priv/templates/message.html
  32. 14 12
      priv/web/rebar.config
  33. 41 0
      priv/web/src/index.erl
  34. 21 0
      priv/web/src/login.erl
  35. 22 0
      priv/web/src/routes.erl
  36. 7 0
      priv/web/src/sample.app.src
  37. 18 0
      priv/web/src/sample.erl
  38. 29 1
      priv/web/sys.config
  39. 12 6
      priv/web/vm.args
  40. 5 6
      src/mad_static.erl

+ 1 - 1
include/mad.hrl

@@ -1 +1 @@
--define(VERSION,"c40ff7").
+-define(VERSION,"a4074f").

+ 5 - 1
index.html

@@ -213,7 +213,11 @@
         </code></figure>
         <a name=docs><h3>BUNDLE</h3></a>
         <figure><code>
- $ mad bundle script review
+ $ mad bundle script sample
+ OK
+        </code></figure>
+        <figure><code>
+ $ mad bundle beam sample
  OK
         </code></figure>
         <div>

BIN
mad


+ 3 - 2
priv/mqtt/priv/www/index.htm → priv/mqtt/priv/static/index.htm

@@ -4,7 +4,7 @@
     <meta charset="utf-8">
     <meta http-equiv="x-ua-compatible" content="ie=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1">
-    <link href='synrc.css?v=103' type='text/css' rel='stylesheet'>
+    <link href='synrc.css?v=210' type='text/css' rel='stylesheet'>
     <title>Chat</title>
 </head>
 
@@ -27,11 +27,12 @@
     <script src="//cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js"></script>
     <script src='/n2o/utf8.js?v=10'></script>
     <script src='/n2o/bert.js?v=10'></script>
+    <script src='/n2o/bullet.js?v=10'></script>
     <script src='/n2o/n2o.js?v=10'></script>
     <script>host = location.hostname === 'review.n2o.space' ? 'ns.synrc.com' : location.hostname; port = 8000;</script>
     <script src='/n2o/ftp.js?v=10'></script>
     <script src='/n2o/nitrogen.js?v=10'></script>
-    <script src='/n2o/mq.js?v=10'></script>
+    <script src='/n2o/mq.js?v=12'></script>
 </body>
 
 </html>

+ 1 - 0
priv/mqtt/priv/www/login.htm → priv/mqtt/priv/static/login.htm

@@ -24,6 +24,7 @@
     <script src="//cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js"></script>
     <script src='/n2o/utf8.js?v=1'></script>
     <script src='/n2o/bert.js?v=1'></script>
+    <script src='/n2o/bullet.js?v=1'></script>
     <script src='/n2o/n2o.js?v=4'></script>
     <script>host = location.hostname === 'review.n2o.space' ? 'ns.synrc.com' : location.hostname; port = 8000;</script>
     <script src='/n2o/ftp.js?v=5'></script>

+ 0 - 0
priv/mqtt/priv/www/n2o/bert.js → priv/mqtt/priv/static/n2o/bert.js


+ 35 - 0
priv/mqtt/priv/static/n2o/bullet.js

@@ -0,0 +1,35 @@
+
+// WebSocket Transport
+
+$ws = { heart: true, interval: 4000,
+        creator: function(url) { return window.WebSocket ? new window.WebSocket(url) : false; },
+        onheartbeat: function() { // this.channel.send('PING'); 
+                                  } };
+
+// N2O Reliable Connection
+
+$conn = { onopen: nop, onmessage: nop, onclose: nop, onconnect: nop,
+          send:  function(data)   { if (this.port.channel) this.port.channel.send(data); },
+          close: function()       { if (this.port.channel) this.port.channel.close(); } };
+
+ct = 0;
+transports = [ $ws ];
+heartbeat = null;
+reconnectDelay = 1000;
+maxReconnects = 100;
+
+function nop() { }
+function bullet(url) { $conn.url = url; return $conn; }
+function xport() { return maxReconnects <= ct ? false : transports[ct++ % transports.length]; }
+function reconnect() { setTimeout(function() { connect(); }, reconnectDelay); }
+function next() { $conn.port = xport(); return $conn.port ? connect() : false; }
+function connect() {
+    $conn.port.channel = $conn.port.creator($conn.url);
+    if (!$conn.port.channel) return next();
+    $conn.port.channel.onmessage = function(e) { $conn.onmessage(e); };
+    $conn.port.channel.onopen = function() {
+        if ($conn.port.heart) heartbeat = setInterval(function(){$conn.port.onheartbeat();}, $conn.port.interval);
+        $conn.onopen();
+        $conn.onconnect(); };
+    $conn.port.channel.onclose = function() { $conn.onclose(); clearInterval(heartbeat); reconnect(); };
+    return $conn; }

+ 0 - 0
priv/mqtt/priv/www/n2o/ftp.js → priv/mqtt/priv/static/n2o/ftp.js


+ 6 - 6
priv/mqtt/priv/www/n2o/mq.js → priv/mqtt/priv/static/n2o/mq.js

@@ -1,8 +1,8 @@
 var match, pl = /\+/g, search = /([^&=]+)=?([^&]*)/g,
-    decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
+    decode_uri = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
     query = window.location.search.substring(1),
     nodes = 4,
-    params = {}; while (match = search.exec(query)) params[decode(match[1])] = decode(match[2]);
+    params = {}; while (match = search.exec(query)) params[decode_uri(match[1])] = decode_uri(match[2]);
 var l = location.pathname,
     x = l.substring(l.lastIndexOf("/") + 1),
     ll = x.lastIndexOf("."),
@@ -16,8 +16,8 @@ var ws = { send: function (payload, qos) {
 var subscribeOptions = {
     qos: 2,  // QoS
     invocationContext: { foo: true },  // Passed to success / failure callback
-    onSuccess: function (x) { console.log("N2O Subscribed"); },
-    onFailure: function (m) { console.log("N2O Subscription failed: " + m.errorMessage); },
+    onSuccess: function (x) { console.log("MQTT Subscribe"); },
+    onFailure: function (m) { console.log("MQTT Subscription failed: " + m.errorMessage); },
     timeout: 2 };
 
 var options = {
@@ -25,8 +25,8 @@ var options = {
     userName: module,
     password: token(),
     cleanSession: false,
-    onFailure: function (m) { console.log("N2O Connection failed: " + m.errorMessage); },
-    onSuccess: function ()  { console.log("N2O Connected ");
+    onFailure: function (m) { console.log("MQTT Connection failed: " + m.errorMessage); },
+    onSuccess: function ()  { console.log("MQTT Connect");
                               ws.send(enc(tuple(atom('init'),bin(token()))));
                             } };
 

+ 0 - 0
priv/mqtt/priv/www/n2o/n2o.js → priv/mqtt/priv/static/n2o/n2o.js


+ 0 - 0
priv/mqtt/priv/www/n2o/nitrogen.js → priv/mqtt/priv/static/n2o/nitrogen.js


+ 0 - 0
priv/mqtt/priv/www/n2o/utf8.js → priv/mqtt/priv/static/n2o/utf8.js


+ 1 - 1
priv/mqtt/priv/www/synrc.css → priv/mqtt/priv/static/synrc.css

@@ -7,7 +7,7 @@
 body {
     font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
     color: #737373;
-    background: url('back.jpg') center / cover;
+//    background: url('back.jpg') center / cover;
     margin: 0;
 }
 

+ 1 - 0
priv/mqtt/rebar.config

@@ -4,5 +4,6 @@
 {deps, [{kvs,    ".*", {git, "git://github.com/synrc/kvs",    []}},
         {n2o,    ".*", {git, "git://github.com/synrc/mqtt",   []}},
         {mad,    ".*", {git, "git://github.com/synrc/mad",    []}},
+        {emqttd, ".*", {git, "git://github.com/synrc/emqttd", []}},
         {nitro,  ".*", {git, "git://github.com/synrc/nitro",  []}}
        ]}.

+ 2 - 2
priv/mqtt/src/index.erl

@@ -15,7 +15,7 @@ event(init) ->
     nitro:update(upload,  #upload { id=upload   }),
     nitro:wire("mqtt.subscribe('room/"++Room++"',subscribeOptions);"),
     Topic = iolist_to_binary(["events/1/",Node,"/index/anon/",Id,"/",Token]),
-    n2o:send_reply(<<>>, 2, Topic, term_to_binary(#client{id=Room,data=list})),
+    n2o:send_reply(<<>>, 2, Topic, term_to_binary(#client{data={Room,list}})),
     io:format("Room: ~p~n",[Room]),
     nitro:wire(#jq{target=message,method=[focus,select]});
 
@@ -33,7 +33,7 @@ event(chat) ->
     io:format("Actions: ~p~n",[Actions]),
     n2o:send_reply(ClientId, 2, iolist_to_binary([<<"room/">>,Room]), M);
 
-event(#client{id=Room,data=list}) ->
+event(#client{data={Room,list}}) ->
     [ nitro:insert_top(history, nitro:jse(message_view(E#entry.from,E#entry.media)))
       || E <- lists:reverse(kvs:entries(kvs:get(feed,{room,Room}),entry,30)) ];
 

+ 1 - 1
priv/mqtt/src/sample.app.src

@@ -1,5 +1,5 @@
 {application,sample,
-             [{description,"sample IoT Application"},
+             [{description,"REVIEW TT Application"},
               {vsn,"1.10"},
               {registered,[]},
               {applications,[kernel,stdlib,kvs,n2o]},

+ 2 - 2
priv/mqtt/src/sample.erl

@@ -7,7 +7,7 @@
 main(A)    -> mad:main(A).
 init([])   -> {ok, {{one_for_one, 5, 10}, [spec()]}}.
 start()    -> start(normal,[]).
-start(_,_) -> emqttd_access_control:register_mod(auth, n2o_auth, [[]], 9998),
+start(_,_) -> emqttd_access_control:register_mod(auth, n2o_auth, [[]], 10),
               supervisor:start_link({local,sample},sample,[]).
 stop(_)    -> ok.
 spec()     ->
@@ -22,5 +22,5 @@ spec()     ->
 docroot() ->
     {file, Here} = code:is_loaded(sample),
     Dir = filename:dirname(filename:dirname(Here)),
-    Root = application:get_env(sample, "statics_root", "priv/www"),
+    Root = application:get_env(sample, "statics_root", "priv/static"),
     filename:join([Dir, Root]).

+ 2 - 1
priv/mqtt/sys.config

@@ -4,12 +4,13 @@
              {protocol, http},
              {acceptors, 4},
              {max_clients, 512},
-             {statics_root, "priv/www"},
+             {statics_root, "priv/static"},
              {main_page, "login.html"}
             ]},
    {bert,[{js,"deps/sample/bpe/"},
          {swift,"deps/sample/bpe/"}]},
    {n2o, [{pickler,n2o_secret},
+          {app,sample},
           {formatter,n2o_bert},
           {protocols,[sample_auth,n2o_nitro,n2o_ftp]}]},
    {emq_dashboard, [{listeners_dash,[{http,18083,[{acceptors,4},{max_clients,512}]}]}]},

+ 0 - 3
priv/web/apps/rebar.config

@@ -1,3 +0,0 @@
-{sub_dirs, [ "sample" ]}.
-{lib_dirs, ["../apps"]}.
-{deps_dir, ["../deps"]}.

+ 0 - 21
priv/web/apps/sample/priv/static/spa/index.htm

@@ -1,21 +0,0 @@
-<html><head>
-<link href="/static/synrc.css" type="text/css" rel="stylesheet">
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-<title>N2O</title></head>
-<body>
-
-    <div    id="history"></div>
-    <input  id="message" type="text">
-    <button id="send"    type="button">Chat</button>
-
-    <script> var transition = {pid: '', port: window.location.port };</script>
-    <script src='/n2o/protocols/bert.js'></script>
-    <script src='/n2o/protocols/client.js'></script>
-    <script src='/n2o/protocols/nitrogen.js'></script>
-    <script src='/n2o/validation.js'></script>
-    <script src='/n2o/bullet.js'></script>
-    <script src='/n2o/utf8.js'></script>
-    <script src='/n2o/n2o.js'></script>
-    <script> protos = [ $bert, $client ]; N2O_start(); </script>
-
-</body></html>

+ 0 - 3
priv/web/apps/sample/priv/static/synrc.css

@@ -1,3 +0,0 @@
-body { font: -webkit-small-control; font-size: 20pt; }
-input { font-size: 18pt; padding: 10px; }
-button { font-size:20pt; padding: 10px; margin-top: 2px; }

+ 0 - 20
priv/web/apps/sample/priv/templates/index.html

@@ -1,20 +0,0 @@
-<html>
-<head>
-<link href="/static/synrc.css" type="text/css" rel="stylesheet">
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-<title>{{title}}</title>
-</head>
-<body>
-{{body}}
-<script>{{script}}</script>
-    <script src='/n2o/protocols/bert.js'></script>
-    <script src='/n2o/protocols/client.js'></script>
-    <script src='/n2o/protocols/nitrogen.js'></script>
-    <script src='/n2o/validation.js'></script>
-    <script src='/n2o/bullet.js'></script>
-    <script src='/n2o/utf8.js'></script>
-    <script src='/n2o/template.js'></script>
-    <script src='/n2o/n2o.js'></script>
-<script>protos = [ $bert, $client ]; N2O_start();</script>
-</body>
-</html>

+ 0 - 7
priv/web/apps/sample/rebar.config

@@ -1,7 +0,0 @@
-{erlydtl_opts, [
-    {doc_root,   "priv/templates"},
-    {out_dir,    "ebin"},
-    {compiler_options, [report, return, debug_info]},
-    {source_ext, ".html"},
-    {module_ext, "_view"}
-]}.

+ 0 - 15
priv/web/apps/sample/src/index.erl

@@ -1,15 +0,0 @@
--module(index).
--compile(export_all).
--include_lib("nitro/include/nitro.hrl").
--include_lib("n2o/include/wf.hrl").
-
-peer()    -> wf:to_list(wf:peer(?REQ)).
-message() -> wf:js_escape(wf:html_encode(wf:to_list(wf:q(message)))).
-main()    -> #dtl{file="index",app=sample,bindings=[{body,body()}]}.
-body()    -> [ #panel{id=history}, #textbox{id=message},
-               #button{id=send,body="Chat",postback=chat,source=[message]} ].
-
-event(init) -> wf:reg(room);
-event(chat) -> wf:send(room,{client,{peer(),message()}});
-event({client,{P,M}}) -> wf:insert_bottom(history,#panel{id=history,body=[P,": ",M,#br{}]});
-event(Event) -> wf:info(?MODULE,"Unknown Event: ~p~n",[Event]).

+ 0 - 20
priv/web/apps/sample/src/routes.erl

@@ -1,20 +0,0 @@
--module(routes).
--include_lib("n2o/include/wf.hrl").
--export([init/2, finish/2]).
-
-finish(State, Ctx) -> {ok, State, Ctx}.
-init(State, Ctx) ->
-    Path = wf:path(Ctx#cx.req),
-    Module = prefix(Path),
-    wf:info(?MODULE,"Route: ~p~n",[Path]),
-    {ok, State, Ctx#cx{path=Path,module=Module}}.
-
-prefix(<<"/ws/",P/binary>>) -> route(P);
-prefix(<<"/",P/binary>>)    -> route(P);
-prefix(P)                   -> route(P).
-
-route(<<>>)                 -> index;
-route(<<"index">>)          -> index;
-route(<<"favicon.ico">>)    -> index;
-route(<<"static/spa/index",_/binary>>) -> index;
-route(_)                    -> index.

+ 0 - 7
priv/web/apps/sample/src/sample.app.src

@@ -1,7 +0,0 @@
-{application, sample,
-  [{description, "N2O Sample"},
-   {vsn, "2.10"},
-   {registered, []},
-   {applications, [kernel, stdlib, n2o]},
-   {mod, { sample, []}},
-   {env, []} ]}.

+ 0 - 26
priv/web/apps/sample/src/sample.erl

@@ -1,26 +0,0 @@
--module(sample).
--behaviour(supervisor).
--behaviour(application).
--export([init/1, start/2, stop/1, main/1]).
--compile(export_all).
-
-main(A)    -> mad_repl:sh(A).
-start(_,_) -> supervisor:start_link({local,sample},sample,[]).
-stop(_)    -> ok.
-init([])   -> case cowboy:start_http(http,3,port(),env()) of
-                   {ok, _}   -> ok;
-                   {error,_} -> halt(abort,[]) end, sup().
-
-sup()    -> { ok, { { one_for_one, 5, 100 }, [] } }.
-env()    -> [ { env, [ { dispatch, points() } ] } ].
-static() ->   { dir, "apps/sample/priv/static", mime() }.
-n2o()    ->   { dir, "deps/n2o/priv",           mime() }.
-mime()   -> [ { mimetypes, cow_mimetypes, all   } ].
-port()   -> [ { port, wf:config(n2o,port,8001)  } ].
-points() -> cowboy_router:compile([{'_', [
-              { "/static/[...]", n2o_static, static() },
-              { "/n2o/[...]",    n2o_static, n2o()    },
-              { "/ws/[...]",     n2o_stream, []       },
-              { '_',             n2o_cowboy, []       }]}]).
-
-log_modules() -> [n2o_client,n2o_nitrogen,n2o_stream,wf_convert].

+ 41 - 0
priv/web/priv/static/index.htm

@@ -0,0 +1,41 @@
+<html>
+
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link href='synrc.css?v=202' type='text/css' rel='stylesheet'>
+    <title>Chat</title>
+</head>
+
+<body class="chat">
+    <header>
+        <a href="/app/login.htm"><img src="https://n2o.space/img/Synrc%20Neo.svg"></a>
+        <h2 id=heading>room</h2>
+        <button id=logout>logout</button>
+    </header>
+    <main>
+        <form>
+            <textarea id=message rows='4' autofocus placeholder="Just type what you think about this"></textarea>
+            <button id=upload>Browse</button>
+            <button id=send>chat</button>
+        </form>
+        <history id="history">
+        </history>
+
+    </main>
+       <script>host = location.hostname === 'sample.n2o.space' ? 'ns.synrc.com' : location.hostname;
+               port = 8001;</script>
+        <script src='/n2o/bert.js'></script>
+        <script src='/n2o/client.js'></script>
+        <script src='/n2o/nitrogen.js'></script>
+        <script src='/n2o/bullet.js'></script>
+        <script src='/n2o/utf8.js'></script>
+        <script src='/n2o/template.js'></script>
+        <script src='/n2o/n2o.js'></script>
+        <script src='/n2o/validation.js'></script>
+        <script src='/n2o/ftp.js'></script>
+        <script>protos = [$client,$bert]; N2O_start();</script>
+</body>
+
+</html>

+ 38 - 0
priv/web/priv/static/login.htm

@@ -0,0 +1,38 @@
+<html>
+
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link href='synrc.css?v=202' type='text/css' rel='stylesheet'>
+    <title>Login</title>
+</head>
+
+<body class="login">
+    <header>
+        <a href="/app/index.htm">
+        <img src="https://n2o.space/img/Synrc%20Neo.svg" style="margin-bottom:40px;"></a>
+    </header>
+    <form>
+        <label for="user">Choose Nickname:</label>
+        <input type="text" id="user" autofocus>
+        <label for="pass">Join or Create Channel:</label>
+        <input type="text" id="pass">
+        <button id="loginButton">login</button>
+    </form>
+    <footer>Brought to you with ♡ by N2O developers.</footer>
+       <script>host = location.hostname === 'sample.n2o.space' ? 'ns.synrc.com' : location.hostname;
+               port = 8001;</script>
+        <script src='/n2o/bert.js'></script>
+        <script src='/n2o/client.js'></script>
+        <script src='/n2o/nitrogen.js'></script>
+        <script src='/n2o/bullet.js'></script>
+        <script src='/n2o/utf8.js'></script>
+        <script src='/n2o/template.js'></script>
+        <script src='/n2o/n2o.js'></script>
+        <script src='/n2o/validation.js'></script>
+        <script src='/n2o/ftp.js'></script>
+        <script>protos = [$bert]; N2O_start();</script>
+</body>
+
+</html>

+ 202 - 0
priv/web/priv/static/synrc.css

@@ -0,0 +1,202 @@
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+    color: #737373;
+//    background: url('back.jpg') center / cover;
+    margin: 0;
+}
+
+button {
+    padding: 10px 20px;
+    font-size: 18px;
+    color: white;
+    background: #4990E2;
+    font-weight: bold;
+    font-family: inherit;
+    line-height: 1;
+    border: none;
+    text-transform: uppercase;
+    border-radius: 4px;
+    box-shadow: 0px 10px 15px -5px rgba(100, 100, 100, .35);
+    margin: 10px;
+}
+
+button:hover {
+    background: #1a84ff;
+}
+
+button:focus {
+    outline: none;
+    background: #1f63b2;
+}
+
+img {
+    width: 140px;
+    margin: auto;
+    padding: 0;
+    display: block;
+}
+
+.login {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    align-items: center;
+}
+
+.login h1 {
+    color: #4990E2;
+    font-size: 34px;
+    margin: 30px 0;
+    text-shadow: 1px 2px 8px rgba(100, 100, 100, .3);
+}
+
+.login img {
+    margin-top: 20px;
+}
+
+.login form {
+    width: 300px;
+    background: white;
+    padding: 20px;
+    display: flex;
+    justify-content: center;
+    flex-wrap: wrap;
+    border-radius: 4px;
+    box-shadow: 0px 4px 40px rgba(100, 100, 100, .35);
+}
+
+.login label {
+    width: 100%;
+    display: block;
+    font-size: 18px;
+}
+
+.login input {
+    width: 100%;
+    display: block;
+    border: none;
+    font-family: inherit;
+    color: inherit;
+    font-size: 19px;
+    line-height: 1.4;
+    border-bottom: 1px solid #4990E2;
+    margin-bottom: 20px;
+}
+
+.login input:focus {
+    outline: none;
+}
+
+.login footer {
+    margin-top: auto;
+    margin-bottom: 30px;
+    font-size: 18px;
+    background: white;
+    padding: 4px 10px;
+    border-radius: 4px;
+    box-shadow: 0px 4px 40px rgba(100, 100, 100, .35);
+}
+
+.chat {
+    text-align: center;
+}
+
+.chat header {
+    display: inline-block;
+    max-width: 200px;
+    vertical-align: top;
+    margin-right: 50px;
+    margin-top: 20px;
+}
+
+.chat img {
+    margin-bottom: 20px;
+}
+
+.chat h2 {
+    color: #4990E2;
+    font-size: 28px;
+    margin: 10px 0;
+    text-shadow: 1px 2px 8px rgba(100, 100, 100, .3);
+}
+
+.chat main {
+    display: inline-block;
+    vertical-align: top;
+    text-align: left;
+    max-width: 600px;
+    margin: 40px 10px;
+}
+
+.chat form {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    margin: 0px 0 70px;
+}
+
+.chat textarea:focus {
+    outline: none;
+}
+
+.chat textarea {
+    width: 100% !important;
+    margin-bottom: 20px;
+    border: none;
+    font-family: inherit;
+    color: inherit;
+    font-size: 18px;
+    line-height: 1.2;
+    padding: 8px 16px;
+    border-radius: 4px;
+    box-shadow: 0px 4px 40px rgba(100, 100, 100, .35);
+}
+
+::-webkit-input-placeholder {
+    color: #ccc;
+}
+
+::-moz-placeholder {
+    color: #ccc;
+}
+
+::-ms-input-placeholder {
+    color: #ccc;
+}
+
+.chat span>button {
+    margin-right: 5px;
+}
+
+history {
+    display: block;
+}
+
+message {
+    display: block;
+    font-size: 18px;
+    background: white;
+    padding: 8px 16px;
+    margin: 15px 0;
+    border-radius: 4px;
+    box-shadow: 0px 4px 40px rgba(100, 100, 100, .35);
+}
+
+author {
+    display: inline;
+    font-weight: bold;
+}
+
+author:empty {
+    display: none;
+}
+
+author::after {
+    content: ': ';
+}

+ 1 - 0
priv/web/priv/templates/message.html

@@ -0,0 +1 @@
+<message><author>{{user}}</author>{{message}}</message>

+ 14 - 12
priv/web/rebar.config

@@ -1,14 +1,16 @@
-{sub_dirs,["apps"]}.
-{deps_dir,"deps"}.
 {deps, [
-    {erlydtl,".*", {git, "git://github.com/voxoz/erlydtl",   []  }},
-    {cowboy, ".*", {git, "git://github.com/voxoz/cowboy",    []  }},
-    {gproc,  ".*", {git, "git://github.com/voxoz/gproc",     []  }},
-    {fs,     ".*", {git, "git://github.com/synrc/fs",        {tag, "4.10"} }},
-    {sh,     ".*", {git, "git://github.com/synrc/sh",        {tag, "2.10"} }},
-    {mad,    ".*", {git, "git://github.com/synrc/mad",       {tag, "4.10"} }},
-    {active, ".*", {git, "git://github.com/synrc/active",    {tag, "4.10"} }},
-    {nitro,  ".*", {git, "git://github.com/synrc/nitro",     {tag, "3.10"} }},
-    {n2o,    ".*", {git, "git://github.com/synrc/n2o",       {tag, "5.10"} }}
+    {gproc,  ".*", {git, "git://github.com/voxoz/gproc",        []}},
+    {cowboy, ".*", {git, "git://github.com/voxoz/cowboy2",      []}},
+    {erlydtl,".*", {git, "git://github.com/voxoz/erlydtl",      []}},
+    {active, ".*", {git, "git://github.com/synrc/active",       []}},
+    {nitro,  ".*", {git, "git://github.com/synrc/nitro",        []}},
+    {n2o,    ".*", {git, "git://github.com/synrc/mqtt",         []}},
+    {kvs,    ".*", {git, "git://github.com/synrc/kvs",          []}}
+]}.
+{erlydtl_opts, [
+    {doc_root,   "priv/templates"},
+    {out_dir,    "ebin"},
+    {compiler_options, [report, return, debug_info]},
+    {source_ext, ".html"},
+    {module_ext, "_view"}
 ]}.
-

+ 41 - 0
priv/web/src/index.erl

@@ -0,0 +1,41 @@
+-module(index).
+-compile(export_all).
+-include_lib("kvs/include/entry.hrl").
+-include_lib("nitro/include/nitro.hrl").
+-include_lib("n2o/include/n2o.hrl").
+body() -> [].
+main() -> [].
+event(init) ->
+    Room = n2o:session(room),
+    n2o:reg({topic,Room}),
+    nitro:update(logout,#button{id=logout, body="Logout " ++ n2o:user(), postback=logout}),
+    nitro:update(heading, #h2     { id=heading, body=Room}),
+    nitro:update(upload,#upload{id=upload}),
+    nitro:update(send, #button{ id=send, body= <<"Chat">>, postback=chat, source=[message] }),
+    [ event({client,{E#entry.from,E#entry.media}})
+      || E <- kvs:entries(kvs:get(feed,{room,Room}),entry,10) ];
+event(logout) ->
+    n2o:user([]),
+    nitro:redirect("/app/login.htm");
+event(chat) ->
+    User    = n2o:user(),
+    Room    = n2o:session(room),
+    Message = n2o:q(message),
+    n2o:info(?MODULE,"Chat pressed: ~p~n",[{Room,Message,User}]),
+    kvs:add(#entry{id=kvs:next_id("entry",1),from=n2o:user(),
+                   feed_id={room,Room},media=Message}),
+    n2o:send({topic,Room},#client{data={User,Message}});
+event(#client{data={User,Message}}) ->
+    HTML = nitro:to_list(Message),
+    nitro:wire(#jq{target=message,method=[focus,select]}),
+    DTL = #dtl{file="message",app=sample,bindings=[{user,User},{color,"gray"},{message,HTML}]},
+    nitro:insert_top(history, nitro:jse(nitro:render(DTL)));
+event(#ftp{sid=Sid,filename=Filename,status={event,stop}}=Data) ->
+    n2o:info(?MODULE,"FTP Delivered ~p~n",[Data]),
+    Name = hd(lists:reverse(string:tokens(nitro:to_list(Filename),"/"))),
+    erlang:put(message,nitro:render(#link{href=iolist_to_binary(["/app/",Sid,"/",nitro:url_encode(Name)]),body=Name})),
+    n2o:info(?MODULE,"Message ~p~n",[wf:q(message)]),
+    event(chat);
+event(Event) ->
+    n2o:info(?MODULE,"Event: ~p", [Event]),
+    ok.

+ 21 - 0
priv/web/src/login.erl

@@ -0,0 +1,21 @@
+-module(login).
+-compile(export_all).
+-include_lib("kvs/include/feed.hrl").
+-include_lib("nitro/include/nitro.hrl").
+-include_lib("n2o/include/n2o.hrl").
+
+main() -> [].
+body() -> [].
+event(init) ->
+    nitro:update(loginButton,
+      #button{id=loginButton,
+              body="Login",postback=login,source=[user,pass]});
+event(login) ->
+    User = nitro:to_list(n2o:q(user)),
+    Room = nitro:to_list(n2o:q(pass)),
+    n2o:user(User),
+    n2o:session(room,Room),
+    n2o:info(?MODULE,"User: ~p",[User]),
+    nitro:redirect("/app/index.htm?room="++Room),
+    ok;
+event(_) -> [].

+ 22 - 0
priv/web/src/routes.erl

@@ -0,0 +1,22 @@
+-module(routes).
+-include_lib("n2o/include/n2o.hrl").
+-export([init/2, finish/2]).
+
+finish(State, Ctx) -> {ok, State, Ctx}.
+init(State, #cx{req=Req}=Cx) ->
+    Path = case application:get_env(n2o,cowboy_spec,cow1) of
+                cow1 -> n2o_cowboy:path(Req); % cowboy 1.0
+                cow2 -> #{path:=P}=Req, P     % cowboy 2.5
+           end,
+    Fix  = route_prefix(Path),
+    n2o:info(?MODULE,"Route: ~p~n",[{Fix,Path}]),
+    {ok, State, Cx#cx{path=Path,module=Fix}}.
+
+route_prefix(<<"/ws/",P/binary>>) -> route(P);
+route_prefix(<<"/",P/binary>>) -> route(P);
+route_prefix(P) -> route(P).
+
+route(<<>>)              -> login;
+route(<<"app/index",_/binary>>) -> index;
+route(<<"app/login",_/binary>>) -> login;
+route(_) -> login.

+ 7 - 0
priv/web/src/sample.app.src

@@ -0,0 +1,7 @@
+{application, sample,
+    [{description, "REVIEW WS Application"},
+     {vsn, "5.11"},
+     {registered, []},
+     {applications, [public_key, asn1, kernel, stdlib, kvs, cowboy, n2o]},
+     {mod, { sample, []}},
+     {env, []}]}.

+ 18 - 0
priv/web/src/sample.erl

@@ -0,0 +1,18 @@
+-module(sample).
+-behaviour(supervisor).
+-behaviour(application).
+-compile(export_all).
+main(A)    -> mad:main(A).
+stop(_)    -> ok.
+start()    -> start(normal,[]).
+start(_,_) -> case ver() of cow1 -> []; _ ->
+                   cowboy:start_clear(http, [{port, port()}],
+                      #{ env => #{dispatch => n2o_cowboy2:points()} })
+              end, supervisor:start_link({local,sample},sample,[]).
+init([])   -> kvs:join(), {ok, {{one_for_one, 5, 10}, ?MODULE:(ver())() }}.
+ver()      -> application:get_env(n2o,cowboy_spec,cow2).
+cow2()     -> [].
+cow1()     -> [spec()].
+port()     -> application:get_env(n2o,port,8001).
+env()      -> [ { env, [ { dispatch, n2o_cowboy:points() } ] } ].
+spec()     -> ranch:child_spec(http,100,ranch_tcp,[{port,port()}],cowboy_protocol,env()).

+ 29 - 1
priv/web/sys.config

@@ -1 +1,29 @@
-[{n2o, [{port,8001},{route,routes},{log_modules,sample}]}].
+[
+ {n2o, [{port,8001},
+        {cowboy_spec,cow2},
+        {app,sample},
+        {upload,"./priv/static/"},
+        {mode,dev},
+        {routes,routes},
+        {mq,n2o_gproc},
+        {formatter,n2o_bert},
+        {protocols,[n2o_heart,n2o_nitro,n2o_ftp]},
+        {minify,{"priv/static",
+                ["deps/n2o/priv/bullet.js",
+                 "deps/n2o/priv/n2o.js",
+                 "deps/n2o/priv/ftp.js",
+                 "deps/n2o/priv/bert.js",
+                 "deps/n2o/priv/nitrogen.js",
+                 "deps/n2o/priv/utf8.js"]}},
+        {log_modules,[config,n2o,review,index,login,routes,n2o_heart,
+                      n2o_nitro,n2o_proto,n2o_cowboy,n2o_stream]},
+        {log_backend,n2o_io},
+        {session,n2o_session},
+        {origin,<<"*">>},
+        {bridge,n2o_cowboy},
+        {pickler,n2o_secret},
+        {erroring,n2o_error},
+        {event,pickle}]},
+ {kvs, [{dba,store_mnesia},
+        {schema, [kvs_user, kvs_acl, kvs_feed, kvs_subscription ]} ]}
+].

+ 12 - 6
priv/web/vm.args

@@ -1,7 +1,13 @@
--name sample@127.0.0.1
-+pc unicode
--setcookie node_runner
++W w
+-kernel net_ticktime 60
+-env ERL_CRASH_DUMP log/crash.dump
++e 256000
+-env ERL_FULLSWEEP_AFTER 1000
++Q 65536
++P 256000
++A 32
 +K true
-+A 5
--env ERL_MAX_PORTS 4096
--env ERL_FULLSWEEP_AFTER 10
+-smp auto
+-setcookie emq_dist_cookie
+-name mq@127.0.0.1
++zdbbl 32768

+ 5 - 6
src/mad_static.erl

@@ -14,22 +14,21 @@ main(_Config, ["min"]) ->
                     {error,"Minifier."}
     end.
 
+replace(S,A,B) -> re:replace(S,A,B,[global,{return,list}]).
+
 app([]) -> app(["web","sample"]);
 app([Name]) -> app(["web",Name]);
 app([Skeleton,Name|_]) ->
-    io:format("Scaffolding ~p Name ~p~n",[Skeleton,Name]),
     mad_repl:load(),
     Apps = ets:tab2list(filesystem),
     try
     [ begin
        case string:str(File,"priv/"++Skeleton) of
-       1 -> Relative = unicode:characters_to_list(Name++
-                       string:replace(
-                       string:replace(File, "sample", Name, all),
-                                     "priv/"++Skeleton, "", all), utf8),
+       1 -> Relative = unicode:characters_to_list(
+               Name ++ replace(replace(File,"sample",Name),"priv/"++Skeleton, []), utf8),
             mad:info("Created: ~p~n",[Relative]),
             filelib:ensure_dir(Relative),
-            BinNew = string:replace(Bin, "sample", Name, all),
+            BinNew = replace(Bin, "sample", Name),
             file:write_file(Relative, BinNew);
        _ -> skip
        end end || {File,Bin} <- Apps, is_list(File)],