First version
authorFulup Ar Foll <fulup@iot.bzh>
Mon, 25 Jan 2016 13:37:32 +0000 (14:37 +0100)
committerFulup Ar Foll <fulup@iot.bzh>
Mon, 25 Jan 2016 13:37:32 +0000 (14:37 +0100)
82 files changed:
afm-client/.gitignore [new file with mode: 0644]
afm-client/LICENSE
afm-client/README.md
afm-client/app/Backend/RestApis/AfmMainMockApi.js [new file with mode: 0644]
afm-client/app/Backend/RestApis/PostMockApi.js [new file with mode: 0644]
afm-client/app/Backend/RestApis/TokenMockApi.js [new file with mode: 0644]
afm-client/app/Backend/RestApis/_all.js [new file with mode: 0644]
afm-client/app/Backend/server.js [new file with mode: 0644]
afm-client/app/Frontend/app.js [new file with mode: 0644]
afm-client/app/Frontend/etc/AppConfig.js [new file with mode: 0644]
afm-client/app/Frontend/etc/routes.js [new file with mode: 0644]
afm-client/app/Frontend/favicon.ico [new file with mode: 0644]
afm-client/app/Frontend/images/appli/isnotvalid.png [new file with mode: 0644]
afm-client/app/Frontend/images/appli/istoobig.png [new file with mode: 0644]
afm-client/app/Frontend/images/appli/upload-appli.png [new file with mode: 0644]
afm-client/app/Frontend/images/appli/w3c-widget.png [new file with mode: 0644]
afm-client/app/Frontend/images/audio/istoobig.png [new file with mode: 0644]
afm-client/app/Frontend/images/audio/upload-music.png [new file with mode: 0644]
afm-client/app/Frontend/images/avatars/istoobig.jpg [new file with mode: 0644]
afm-client/app/Frontend/images/avatars/istoobig.png [new file with mode: 0644]
afm-client/app/Frontend/images/avatars/tux-admin.png [new file with mode: 0644]
afm-client/app/Frontend/images/avatars/tux-bzh.png [new file with mode: 0644]
afm-client/app/Frontend/images/avatars/tux-visitor.png [new file with mode: 0644]
afm-client/app/Frontend/images/icons/annex-ico.png [new file with mode: 0644]
afm-client/app/Frontend/images/icons/memorymatch-ico.png [new file with mode: 0644]
afm-client/app/Frontend/images/icons/rabbit-ico.png [new file with mode: 0644]
afm-client/app/Frontend/images/icons/w3c-ico.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/fb-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/gg-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/gh-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/iot-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/lk-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/ms-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/og-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/pp-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/login/yh-logo.png [new file with mode: 0644]
afm-client/app/Frontend/images/logo/logo_iot_bzh.svg [new file with mode: 0644]
afm-client/app/Frontend/images/logo/logo_iot_bzhx350.png [new file with mode: 0644]
afm-client/app/Frontend/images/logo/tampon-iot-bzhx450.png [new file with mode: 0644]
afm-client/app/Frontend/images/logo/triskel_iot_bzh.png [new file with mode: 0644]
afm-client/app/Frontend/images/logo/triskel_iot_bzh.svg [new file with mode: 0644]
afm-client/app/Frontend/images/logo/triskel_iot_bzhx250.png [new file with mode: 0644]
afm-client/app/Frontend/index.html [new file with mode: 0644]
afm-client/app/Frontend/pages/Dashboard/Dashboard.html [new file with mode: 0644]
afm-client/app/Frontend/pages/Dashboard/DashboardModule.js [new file with mode: 0644]
afm-client/app/Frontend/pages/Dashboard/DashboardModule.scss [new file with mode: 0644]
afm-client/app/Frontend/pages/Home/Dashboard.html [new file with mode: 0644]
afm-client/app/Frontend/pages/Home/DashboardModule.js [new file with mode: 0644]
afm-client/app/Frontend/pages/Home/DashboardModule.scss [new file with mode: 0644]
afm-client/app/Frontend/pages/Sample/Sample.html [new file with mode: 0644]
afm-client/app/Frontend/pages/Sample/SampleModule.js [new file with mode: 0644]
afm-client/app/Frontend/pages/Sample/SampleModule.scss [new file with mode: 0644]
afm-client/app/Frontend/services/JQueryEmu.js [new file with mode: 0644]
afm-client/app/Frontend/styles/README.md [new file with mode: 0644]
afm-client/app/Frontend/styles/app/_ibz-mixins.scss [new file with mode: 0644]
afm-client/app/Frontend/styles/app/ibz-global.scss [new file with mode: 0644]
afm-client/app/Frontend/styles/foundation/_foundation-icons.scss [new file with mode: 0644]
afm-client/app/Frontend/styles/foundation/_foundation-settings.scss [new file with mode: 0644]
afm-client/app/Frontend/styles/foundation/foundation-conf.scss [new file with mode: 0644]
afm-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss [new file with mode: 0644]
afm-client/app/Frontend/widgets/ActionButtons/AppliButton.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/ActionButtons/SubmitButton.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/FormInput/FormInput.scss [new file with mode: 0644]
afm-client/app/Frontend/widgets/FormInput/InputPassword.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/FormInput/InputText.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/FormInput/UploadAppli.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/Navigation/LinkButton.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/Navigation/Navigation.scss [new file with mode: 0644]
afm-client/app/Frontend/widgets/Notifications/ModalNotification.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/Notifications/Notifications.scss [new file with mode: 0644]
afm-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js [new file with mode: 0644]
afm-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss [new file with mode: 0644]
afm-client/app/etc/AppDefaults.js [new file with mode: 0644]
afm-client/app/etc/_Config.js [new file with mode: 0644]
afm-client/app/etc/_Trace.js [new file with mode: 0644]
afm-client/bower.json [new file with mode: 0644]
afm-client/gulpfile.js [new file with mode: 0644]
afm-client/index.html [new file with mode: 0644]
afm-client/nbproject/project.properties [new file with mode: 0644]
afm-client/nbproject/project.xml [new file with mode: 0644]
afm-client/package.json [new file with mode: 0644]

diff --git a/afm-client/.gitignore b/afm-client/.gitignore
new file mode 100644 (file)
index 0000000..bb6f2ab
--- /dev/null
@@ -0,0 +1,7 @@
+bower_components/
+node_modules/
+dist.dev/
+dist.prod/
+*.DS_Store
+nbproject/private/
+.noderc*
index 8dada3e..89f9ced 100644 (file)
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "{}"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright {yyyy} {name of copyright owner}
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
index 2936255..babf7ab 100644 (file)
@@ -1,2 +1,98 @@
-# afm-client
-Sample Client for AFM install/start/stop/remove apps
+
+## Installation
+
+Install HTML5 development toolchain on your host
+
+    1. Check out this repository
+       git clone https://github.com/iotbzh/afm-client.git
+
+    2) Install NodeJs [not used on target] 
+       zypper install nodejs
+       yum install nodejs
+
+    3) Install building tools [bower, gulp, ....]
+       npm install # this install all development tool chain dependencies
+       sudo npm install --global gulp  # this is not mandatory but it will make your live simpler
+
+    4. For livereload functionality [automatic refresh of HTML/CSS]
+       install [livereload Chrome extension](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei)
+
+
+### Overload ./app/etc/AppDefaults.js with '.noderc.js'
+    var config= {
+        APPNAME : 'AFBclient',   // AppName is use as main Angular Module name
+        FRONTEND: "Frontend",    // HTML5 frontend  [no leading ./]
+        BACKEND : "Backend",     // NodeJS Rest API [no leading ./]
+        URLBASE : '/opa/',       // HTML basedir when running in production [should end with a /]
+        APIBASE : '/api/',       // Api url base dir [should end with a /]
+        DEBUG   : 5003,          // Node Debug Port [for mock API debug only]
+        DBG_LVL : 5,             // Debug Trace Level 0=no trace.
+    };
+    module.exports = config;
+
+    WARNING: in current version Frontend/services/AppConfig.js is not updated automatically
+    you should make sure than your backend config fit with your frontend config.
+    Note: FCS version should have AppConfig.js configurated automatically from GULP, but this is for "tomorrow"
+
+### Build project
+    gulp help
+    gulp build-app-dev
+    gulp watch-dev 
+    http://localhost:4003/opa  /* debug mock api base on Backend/RestApi */
+
+### Test with Application server binder
+
+    # Start AppFramework Binder
+        export MYWORKSPACE=$HOME/Workspace
+        $MYWORKSPACE/afb-daemon/build/afb-daemon --port=1234 --verbose --token=123456789 --rootdir=$MYWORKSPACE/afm-client/dist.dev
+
+    Point your browser onto: http://localhost:1234/opa
+
+    Note: 
+      - do not forget '/opa' that should match with your config.URLBASE
+      - if you change --token=xxxx do not forget to update ./Frontend/pages/HomeModules.js
+      - Force HTML/OPA reload with F5 after each HTML5/OPA update or new pages may not be loaded. 
+      - When reloading HTML/OPA with F5 do not forget that your initial token wont be accepted anymore. You should either restart to clean existing session or cleanup AJB_session cookie.
+
+### Move to Target
+    cd $MYWORKSPACE/afm-client
+    gulp build-app-prod
+    scp -r ./dist.pro/* user@mytarget:/rootdir/apfDaemon
+
+    /AppClient
+    |
+    |---- package.json
+    |---- bower.json
+    |---- gulpfile.js
+    |---- .noderc.js  [Warning: contains private keys should not uploaded in Github]
+    |
+    |---- /Frontend
+    |     |
+    |     |---- index.html
+    |     |---- app.js
+    |     |
+    |     |---- /styles
+    |     |     |
+    |     |     |---- _settings.scss
+    |     |     |---- app.scss
+    |     |
+    |     |---- /Widgets
+    |     |     |
+    |     |     |--- Widget-1
+    |     |     |...
+    |     |
+    |     |-----/Pages
+    |           |--- Home-Page
+    |           |... 
+    |
+    |
+    |---- /Backend
+    |     |-- server.js     // launcher
+    |     |----/ models     // mogoose database schemas
+    |     |----/ providers  // authentication services
+    |     |----/ restapis   // application APIs
+    |
+    |---- (/dist.dev)
+    |---- (/dist.prod)
+
+    
\ No newline at end of file
diff --git a/afm-client/app/Backend/RestApis/AfmMainMockApi.js b/afm-client/app/Backend/RestApis/AfmMainMockApi.js
new file mode 100644 (file)
index 0000000..0d8cd87
--- /dev/null
@@ -0,0 +1,52 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* ----------------------------------------------------------------------
+ *   This module simulate Application Framework Binder
+ *   
+ *   /api/afm-main/runnables  // no params
+ *   /api/afm-main/details    &id="xxxx"
+ *   /api/afm-main/start      &id="xxxx"
+ *   /api/afm-main/terminate  &id="xxxx"
+ *   /api/afm-main/stop       &id="xxxx"
+ *   /api/afm-main/continue   &id="xxxx"
+ *   /api/afm-main/runners    &id="xxxx"
+ *   /api/afm-main/state      &id="xxxx"
+ * ----------------------------------------------------------------------*/
+
+function NewApi(handle, prefix) {
+    var scope=this; // I hate JavaScript
+    scope.connected=false;
+    
+    // Simulate Client Context Session Creation
+    handle.app.get (prefix +'/runnables', function (req, res) {
+        var Response= { jtype: "AJB_reply",
+                        request: { "prefix": "afm-main", "api": "runnables", "uuid": "e4ef5e66-xxxx", "token": "123456789-xxxxx", "status": "processed" },
+                        response: [ 
+                            {id: "webapps-rabbit@0.0", version: "0.0.8", name: "Rabbit", description: "Fun grid game where the rabbit finds and eats the carrots dodging the foxes.", shortname: "", author: "Todd Brandt <todd.e.brandt@intel.com>" },
+                            {id: "webapps-annex@0.0", version: "0.0.10", name: "Annex", description: "Reversi/Othello", shortname: "", author: "Todd Brandt <todd.e.brandt@intel.com>" },
+                            {id: "webapps-memory-match@1.1", version: "1.1.7", name: "MemoryMatch", description: "Memory match", shortname: "", author: "Todd Brandt <todd.e.brandt@intel.com>"  }
+                       ]};
+                    
+        res.send(Response);
+    });
+}
+
+// Export Class
+module.exports = NewApi;
\ No newline at end of file
diff --git a/afm-client/app/Backend/RestApis/PostMockApi.js b/afm-client/app/Backend/RestApis/PostMockApi.js
new file mode 100644 (file)
index 0000000..022f774
--- /dev/null
@@ -0,0 +1,53 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * References: https://github.com/expressjs/multer
+ */
+
+var fs = require('fs');
+var multer = require('multer');
+function NewApi(handle, prefix) {
+    var scope=this; // make sure not to loose object context in async callback
+    
+    // defined upload directory and check it's a valid one
+    var upload = multer({ dest: handle.config.UPLOAD_DIR});
+    // WARNING: single('avatar') should match with <upload-image name="avatar">
+    handle.app.post(prefix +'/upload-image', upload.single('avatar'), function (req, res) {
+        
+        handle.trace (scope, 1, "%s/upload file=%s dest=%s/%s", prefix, req.file.originalname, req.file.destination, req.file.filename);
+        res.send({"jtype": "TEST_message", "status": "success", "info": "done"});
+    });
+    
+    // WARNING: single('music') should match with <upload-audio name="music">
+    handle.app.post(prefix +'/upload-music', upload.single('music'), function (req, res) {
+        
+        handle.trace (scope, 1, "%s/upload file=%s dest=%s/%s", prefix, req.file.originalname, req.file.destination, req.file.filename);
+        res.send({"jtype": "TEST_message", "status": "success", "info": "done"});
+    });
+    
+    // WARNING: single('appli') should match with <upload-audio name="appli">
+    handle.app.post(prefix +'/upload-appli', upload.single('appli'), function (req, res) {
+        
+        handle.trace (scope, 1, "%s/upload file=%s dest=%s/%s", prefix, req.file.originalname, req.file.destination, req.file.filename);
+        res.send({"jtype": "TEST_message", "status": "success", "info": "done"});
+    });
+    
+}
+
+// Export Class
+module.exports = NewApi;
\ No newline at end of file
diff --git a/afm-client/app/Backend/RestApis/TokenMockApi.js b/afm-client/app/Backend/RestApis/TokenMockApi.js
new file mode 100644 (file)
index 0000000..5ef1cfa
--- /dev/null
@@ -0,0 +1,114 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* ----------------------------------------------------------------------
+ *   This module simulate Application Framework Binder
+ *   
+ *   /api/afbs/create
+ *   /api/afbs/check?token=123456789
+ *   /api/afbs/refresh?token=123456789-xxxxx
+ *   /api/afbs/reset?123456789-xxxxx
+ *   
+ *   Note: this MOCK api does not handle any session login. It only returns 
+ *   a fake valid or false message depending on call order.
+ *   Its goal is to get a quick way to check you HTML5 client rendering & behaviour.
+ *   
+ *   When you're happy with you HTML5 client OnePageApp check it with afb-daemon
+ * ----------------------------------------------------------------------*/
+
+function NewApi(handle, prefix) {
+    var scope=this; // I hate JavaScript
+    scope.connected=false;
+    
+    // Simulate Client Context Session Creation
+    handle.app.get(prefix +'/create', function (req, res) {
+        handle.trace (scope, 1, "%s/create", prefix);
+        var okResponse= '{ "jtype": "AJB_reply"' +
+                        ', "request": { "prefix": "afbs", "api": "create", "uuid": "e4ef5e66-xxxx", "token": "123456789-xxxxx", "status": "processed" }'+
+                        ', "response": { "token": "Token was refreshed" }'+
+                        '}';
+                
+        var fxResponse= '{ "jtype": "AJB_reply" ' +
+                        ', "request": { "prefix": "afbs", "api": "create", "status": "fail", "info": "AFB_SESSION_REFRESH Not Initial Token Chain" }'+
+                        '}';
+    
+        //if (scope.connected) res.status(401).send(fxResponse);
+        //else {
+            res.send(okResponse);
+            scope.connected=true;
+        //}
+    });
+    
+    
+    // Simulate Client Context Check
+    handle.app.get(prefix +'/check', function (req, res) {
+        handle.trace (scope, 1, "%s/check query=%s", prefix, req.query.token);
+        var okResponse= '{"jtype":"AJB_reply"'+
+                        ',"request":{"prefix":"afbs","api":"check", "status":"processed"}'+
+                        ',"response":{"isvalid":true}'+
+                        '}';
+                
+        var fxResponse= '{"jtype":"AJB_reply",'+
+                        '"request":{"prefix":"afbs","api":"check","status":"empty","info":"AFB_SESSION_CHECK Not a Valid Active Token"}'+
+                        '}';
+    
+        if (!scope.connected) res.status(401).send(fxResponse);
+        else res.send(okResponse);
+    });
+    
+    // Simulate Client Context Check
+    handle.app.get(prefix +'/refresh', function (req, res) {
+        handle.trace (scope, 1, "%s/refresh query=%s", prefix, req.query.token);
+        var okResponse= '{"jtype":"AJB_reply"'+
+                        ',"request":{"prefix":"afbs","api":"refresh","uuid": "e4ef5e66-xxxx", "token": "123456789-xxxxx","status":"processed"}'+
+                        ',"response":{"isvalid":true}'+
+                        '}';
+                
+        var fxResponse= '{"jtype":"AJB_reply",'+
+                        '"request":{"prefix":"afbs","api":"refresh","status":"empty","info":"AFB_SESSION_REFRESH Not a Valid Active Token"}'+
+                        '}';
+    
+        if (!scope.connected) res.status(401).send(fxResponse);
+        else res.send(okResponse);
+    });
+
+        // Simulate Client Context Session Closing
+    handle.app.get(prefix +'/reset', function (req, res) {
+        handle.trace (scope, 1, "%s/reset query=%s", prefix, req.query.token);
+        var okResponse= '{"jtype":"AJB_reply"'+
+                        ',"request":{"prefix":"afbs","api":"reset","uuid": "e4ef5e66-xxxx","status":"processed"}'+
+                        ',"response":{"uuid":"b028b883-8b47-4c6d-9c6e-e79b9e2b81b9"}'+
+                        '}';
+                
+        var fxResponse= '{"jtype":"AJB_reply",'+
+                        '"request":{"prefix":"afbs","api":"reset","status":"empty","info":"AFB_SESSION_CLOSE Not a Valid Access Token"}'+
+                        '}';
+    
+        if (!scope.connected) res.status(401).send(fxResponse);
+        else {
+            res.send(okResponse);
+            scope.connected=false;
+        }
+    });
+    
+
+}
+
+// Export Class
+module.exports = NewApi;
\ No newline at end of file
diff --git a/afm-client/app/Backend/RestApis/_all.js b/afm-client/app/Backend/RestApis/_all.js
new file mode 100644 (file)
index 0000000..8fab76a
--- /dev/null
@@ -0,0 +1,29 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+// Include here every application APIs routes modules.
+function Initialise (handle) {
+        
+    new require ('./TokenMockApi') (handle, config.APIBASE + 'token');
+    new require ('./PostMockApi')  (handle, config.APIBASE + 'post');
+    new require ('./AfmMainMockApi')   (handle, config.APIBASE + 'afm-main');
+}
+
+module.exports = Initialise;
+
diff --git a/afm-client/app/Backend/server.js b/afm-client/app/Backend/server.js
new file mode 100644 (file)
index 0000000..11c5486
--- /dev/null
@@ -0,0 +1,58 @@
+var config  = require('../etc/_Config');
+var trace   = require('../etc/_Trace');
+var RestAPI = require('./RestApis/_all');
+var fs      = require('fs');
+
+var express        = require('express');
+var session        = require('express-session');
+var bodyParser     = require('body-parser');
+var methodOverride = require('method-override');
+
+// instanciate express HTTP server
+var app = express();
+
+// chose dev or prod rootdir
+var staticdir = 'dist.dev';
+if (process.env.MODE) staticdir = process.env.MODE === 'prod' ? 'dist.prod' : 'dist.dev';
+else staticdir = config.MODE === 'prod' ? 'dist.prod' : 'dist.dev';
+
+var rootdir = __dirname + '/../../' + staticdir;
+if (!fs.existsSync(rootdir)) {
+    console.log("### HOOPS Rootdir not found rootdir=%s\n", rootdir);
+    process.exit();
+}
+
+// get all data/stuff of the body (POST) parameters
+app.use(bodyParser.json()); // parse application/json
+app.use(methodOverride('X-HTTP-Method-Override')); // override with the X-HTTP-Method-Override header in the request. simulate DELETE/PUT
+
+// This handle should contain enough for application logic
+var serverHandle = {
+  app  :  app,           // Express server
+  config: config,
+  trace:  config.DBG_LVL > 0 ? trace : function(){/*empty function */}
+};
+
+// set the static files location /public/img will be /img for users
+app.use(express.static(rootdir)); 
+
+// Load Mock APIs
+var apirest = new RestAPI(serverHandle);
+
+app.get(config.URLBASE, function (req, res) {
+    console.log ("Angular OPA %s", req.originalUrl);
+    res.sendfile(config.URLBASE +"index.html", {root: rootdir});
+});
+
+// rewrite requested URL to include Angular hashPrompt and set session flag for RestAPI
+app.get(config.URLBASE + '*', function(req, res) {
+    // Warning redirect should be under exact "/opa/#!page" or a redirect to home will be done
+    var redirect=config.URLBASE + '#!' + req.originalUrl.substring(config.URLBASE.length);
+    res.redirect(redirect);
+    console.log ("Redirect to: ", redirect);
+});
+
+
+// start app ===============================================
+app.listen(config.EXPRESS_PORT, config.EXPRESS_HOST);
+console.log('Server Listening http://%s:%d (rootdir=%s)', config.EXPRESS_HOST, config.EXPRESS_PORT, rootdir);
\ No newline at end of file
diff --git a/afm-client/app/Frontend/app.js b/afm-client/app/Frontend/app.js
new file mode 100644 (file)
index 0000000..eac36e4
--- /dev/null
@@ -0,0 +1,64 @@
+(function() {
+  'use strict';
+
+  angular.module('@@APPNAME@@', [ // Warning: Appname should fit with gulpfile.js & index.html
+    'ui.router',
+    'ngAnimate',
+
+    //foundation
+    'foundation',
+    'foundation.dynamicRouting',
+    'foundation.dynamicRouting.animations',
+    
+    // external components
+    'ui-notification',
+    
+    // Application Components
+    'AppConfig',
+    'JQueryEmu',
+    'DashboardModule',
+    'SampleModule',
+    'UploadFiles',
+    'LinkButton',
+    'TokenRefresh',
+    'RangeSlider',
+    'ModalNotification'
+  ])
+    .config(config)
+    .run(run)
+  ;
+
+  config.$inject = ['$urlRouterProvider', '$locationProvider'];
+  
+  console.log ("***location=" + window.location + " search" + window.search);
+
+  function config($urlProvider, $locationProvider, AppConfig) {
+    $urlProvider.otherwise('/dashboard');
+
+    // https://docs.angularjs.org/error/$location/nobase
+    $locationProvider.html5Mode(true).hashPrefix('!');
+    
+  }
+
+  function run() {
+    FastClick.attach(document.body);
+  }
+  
+// Fondation-app.template is not included correctly by gulp 
+// Include here missing templates from foundation-apps/dist/js/foundation-apps-templates.js
+angular.module('foundation').run(['$templateCache', function($templateCache) {
+  $templateCache.put('components/modal/modal.html',
+    '<div\n' +
+    '  class="modal-overlay"\n' +
+    '  ng-click="hideOverlay()">\n' +
+    '  <aside\n' +
+    '    class="modal"\n' +
+    '    ng-click="$event.stopPropagation();"\n' +
+    '    ng-transclude>\n' +
+    '  </aside>\n' +
+    '</div>\n' +
+    '');
+}]);
+
+console.log ("opa=@@APPNAME@@ Loaded");
+})();
diff --git a/afm-client/app/Frontend/etc/AppConfig.js b/afm-client/app/Frontend/etc/AppConfig.js
new file mode 100644 (file)
index 0000000..16a2c87
--- /dev/null
@@ -0,0 +1,55 @@
+(function () {
+    'use strict';
+
+    // _all modules only reference dependencies
+    angular.module('AppConfig', [])
+    
+            // Factory is a singleton and share its context within all instances.
+            .factory('AppConfig', function () {
+
+                // console.log ("URL="+ $location.url() + " Query=" + location.href+ " window=" + document.referrer);
+
+                var myConfig = {
+                    
+                    paths: { // Warning paths should end with /
+                        image : 'images/',
+                        icons : 'images/icons/',
+                        avatar: 'images/avatars/',
+                        audio : 'images/audio/'
+                    },
+                    
+                    myapi: { // Warning paths should end with /
+                       token : '/api/myplugin/xxxx'
+                    },
+                    
+                    session: { // Those data are updated by session service
+                       create  : '/api/token/create',
+                       refresh : '/api/token/refresh',
+                       check   : '/api/token/check',
+                       reset   : '/api/token/reset',
+                       ping    : '/api/token/check',
+                       initial : '123456789',  // typical dev initial token
+                       timeout : 3600,         // timeout is updated client sessin context creation
+                       pingrate: 60,           // Ping rate to check if server is still alive
+                       uuid    : '',           // uuid map with cookie or long term session access key
+                       token   : ''            // will be returned from authentication    
+                    }
+                };
+
+                return myConfig;
+            })
+
+            // Factory is a singleton and share its context within all instances.
+            .factory('AppCall', function ($http, AppConfig) {
+                var myCalls = {
+                    get : function(plugin, action, query, callback) {
+                        query["token"] = AppConfig.session.token; // add token to provided query                        
+                        $http.get('/api/' + plugin + '/' + action , {params: query}).then (callback, callback);
+                    }
+
+                };
+                return myCalls;
+            });
+    
+})();
\ No newline at end of file
diff --git a/afm-client/app/Frontend/etc/routes.js b/afm-client/app/Frontend/etc/routes.js
new file mode 100644 (file)
index 0000000..18d281a
--- /dev/null
@@ -0,0 +1 @@
+var foundationRoutes = [{"name":"mysample","url":"/sample","controller":"SampleController as ctrl","animationIn":"slideInRight","path":"pages/Sample/Sample.html"},{"name":"Dashboard","url":"/dashboard","controller":"DashboardController as ctrl","animationIn":"slideInRight","path":"pages/Dashboard/Dashboard.html"},{"name":"dashboard","url":"/dashboard","controller":"DashboardController as ctrl","animationIn":"slideInRight","path":"pages/Home/Dashboard.html"}]; 
diff --git a/afm-client/app/Frontend/favicon.ico b/afm-client/app/Frontend/favicon.ico
new file mode 100644 (file)
index 0000000..eeb7ab7
Binary files /dev/null and b/afm-client/app/Frontend/favicon.ico differ
diff --git a/afm-client/app/Frontend/images/appli/isnotvalid.png b/afm-client/app/Frontend/images/appli/isnotvalid.png
new file mode 100644 (file)
index 0000000..057c215
Binary files /dev/null and b/afm-client/app/Frontend/images/appli/isnotvalid.png differ
diff --git a/afm-client/app/Frontend/images/appli/istoobig.png b/afm-client/app/Frontend/images/appli/istoobig.png
new file mode 100644 (file)
index 0000000..5614073
Binary files /dev/null and b/afm-client/app/Frontend/images/appli/istoobig.png differ
diff --git a/afm-client/app/Frontend/images/appli/upload-appli.png b/afm-client/app/Frontend/images/appli/upload-appli.png
new file mode 100644 (file)
index 0000000..a35fd3a
Binary files /dev/null and b/afm-client/app/Frontend/images/appli/upload-appli.png differ
diff --git a/afm-client/app/Frontend/images/appli/w3c-widget.png b/afm-client/app/Frontend/images/appli/w3c-widget.png
new file mode 100644 (file)
index 0000000..74d3927
Binary files /dev/null and b/afm-client/app/Frontend/images/appli/w3c-widget.png differ
diff --git a/afm-client/app/Frontend/images/audio/istoobig.png b/afm-client/app/Frontend/images/audio/istoobig.png
new file mode 100644 (file)
index 0000000..5614073
Binary files /dev/null and b/afm-client/app/Frontend/images/audio/istoobig.png differ
diff --git a/afm-client/app/Frontend/images/audio/upload-music.png b/afm-client/app/Frontend/images/audio/upload-music.png
new file mode 100644 (file)
index 0000000..2006ef0
Binary files /dev/null and b/afm-client/app/Frontend/images/audio/upload-music.png differ
diff --git a/afm-client/app/Frontend/images/avatars/istoobig.jpg b/afm-client/app/Frontend/images/avatars/istoobig.jpg
new file mode 100644 (file)
index 0000000..da0f255
Binary files /dev/null and b/afm-client/app/Frontend/images/avatars/istoobig.jpg differ
diff --git a/afm-client/app/Frontend/images/avatars/istoobig.png b/afm-client/app/Frontend/images/avatars/istoobig.png
new file mode 100644 (file)
index 0000000..5614073
Binary files /dev/null and b/afm-client/app/Frontend/images/avatars/istoobig.png differ
diff --git a/afm-client/app/Frontend/images/avatars/tux-admin.png b/afm-client/app/Frontend/images/avatars/tux-admin.png
new file mode 100644 (file)
index 0000000..ee40d2a
Binary files /dev/null and b/afm-client/app/Frontend/images/avatars/tux-admin.png differ
diff --git a/afm-client/app/Frontend/images/avatars/tux-bzh.png b/afm-client/app/Frontend/images/avatars/tux-bzh.png
new file mode 100644 (file)
index 0000000..16e001b
Binary files /dev/null and b/afm-client/app/Frontend/images/avatars/tux-bzh.png differ
diff --git a/afm-client/app/Frontend/images/avatars/tux-visitor.png b/afm-client/app/Frontend/images/avatars/tux-visitor.png
new file mode 100644 (file)
index 0000000..bd491b0
Binary files /dev/null and b/afm-client/app/Frontend/images/avatars/tux-visitor.png differ
diff --git a/afm-client/app/Frontend/images/icons/annex-ico.png b/afm-client/app/Frontend/images/icons/annex-ico.png
new file mode 100644 (file)
index 0000000..8e5aea9
Binary files /dev/null and b/afm-client/app/Frontend/images/icons/annex-ico.png differ
diff --git a/afm-client/app/Frontend/images/icons/memorymatch-ico.png b/afm-client/app/Frontend/images/icons/memorymatch-ico.png
new file mode 100644 (file)
index 0000000..4041084
Binary files /dev/null and b/afm-client/app/Frontend/images/icons/memorymatch-ico.png differ
diff --git a/afm-client/app/Frontend/images/icons/rabbit-ico.png b/afm-client/app/Frontend/images/icons/rabbit-ico.png
new file mode 100644 (file)
index 0000000..9dec018
Binary files /dev/null and b/afm-client/app/Frontend/images/icons/rabbit-ico.png differ
diff --git a/afm-client/app/Frontend/images/icons/w3c-ico.png b/afm-client/app/Frontend/images/icons/w3c-ico.png
new file mode 100644 (file)
index 0000000..4e97980
Binary files /dev/null and b/afm-client/app/Frontend/images/icons/w3c-ico.png differ
diff --git a/afm-client/app/Frontend/images/login/fb-logo.png b/afm-client/app/Frontend/images/login/fb-logo.png
new file mode 100644 (file)
index 0000000..fcf7847
Binary files /dev/null and b/afm-client/app/Frontend/images/login/fb-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/gg-logo.png b/afm-client/app/Frontend/images/login/gg-logo.png
new file mode 100644 (file)
index 0000000..0c372eb
Binary files /dev/null and b/afm-client/app/Frontend/images/login/gg-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/gh-logo.png b/afm-client/app/Frontend/images/login/gh-logo.png
new file mode 100644 (file)
index 0000000..ff856fc
Binary files /dev/null and b/afm-client/app/Frontend/images/login/gh-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/iot-logo.png b/afm-client/app/Frontend/images/login/iot-logo.png
new file mode 100644 (file)
index 0000000..ca594d7
Binary files /dev/null and b/afm-client/app/Frontend/images/login/iot-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/lk-logo.png b/afm-client/app/Frontend/images/login/lk-logo.png
new file mode 100644 (file)
index 0000000..d9bc51f
Binary files /dev/null and b/afm-client/app/Frontend/images/login/lk-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/ms-logo.png b/afm-client/app/Frontend/images/login/ms-logo.png
new file mode 100644 (file)
index 0000000..d4f23eb
Binary files /dev/null and b/afm-client/app/Frontend/images/login/ms-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/og-logo.png b/afm-client/app/Frontend/images/login/og-logo.png
new file mode 100644 (file)
index 0000000..a6f6e9a
Binary files /dev/null and b/afm-client/app/Frontend/images/login/og-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/pp-logo.png b/afm-client/app/Frontend/images/login/pp-logo.png
new file mode 100644 (file)
index 0000000..dbb8866
Binary files /dev/null and b/afm-client/app/Frontend/images/login/pp-logo.png differ
diff --git a/afm-client/app/Frontend/images/login/yh-logo.png b/afm-client/app/Frontend/images/login/yh-logo.png
new file mode 100644 (file)
index 0000000..6ab90cf
Binary files /dev/null and b/afm-client/app/Frontend/images/login/yh-logo.png differ
diff --git a/afm-client/app/Frontend/images/logo/logo_iot_bzh.svg b/afm-client/app/Frontend/images/logo/logo_iot_bzh.svg
new file mode 100644 (file)
index 0000000..6e60c95
--- /dev/null
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="597.39423"
+   height="162.54224"
+   id="svg4035"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="New document 5">
+  <defs
+     id="defs4037">
+    <filter
+       color-interpolation-filters="sRGB"
+       id="filter4000"
+       inkscape:label="Drop Shadow">
+      <feFlood
+         id="feFlood4002"
+         flood-opacity="0.475"
+         flood-color="rgb(0,0,0)"
+         result="flood" />
+      <feComposite
+         id="feComposite4004"
+         in2="SourceGraphic"
+         in="flood"
+         operator="in"
+         result="composite1" />
+      <feGaussianBlur
+         id="feGaussianBlur4006"
+         stdDeviation="5"
+         result="blur" />
+      <feOffset
+         id="feOffset4008"
+         dx="8"
+         dy="8"
+         result="offset" />
+      <feComposite
+         id="feComposite4010"
+         in2="offset"
+         in="SourceGraphic"
+         operator="over"
+         result="composite2" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="339.36637"
+     inkscape:cy="3.4393101"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1108"
+     inkscape:window-height="862"
+     inkscape:window-x="321"
+     inkscape:window-y="159"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata4040">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-49.874326,-433.94821)">
+    <g
+       id="g3091"
+       transform="translate(62.857151,24.927697)"
+       inkscape:export-filename="/home/sdx/Pictures/Logo/logo_iot_bzh_100dpi.png"
+       inkscape:export-xdpi="100.22011"
+       inkscape:export-ydpi="100.22011"
+       style="display:inline;filter:url(#filter4000)">
+      <text
+         sodipodi:linespacing="125%"
+         id="text3557-5-3-7-0-7-3"
+         y="519.50671"
+         x="27.886671"
+         style="font-size:97.09867096px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:Sans"
+         xml:space="preserve"><tspan
+           style="font-size:97.09867096px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;font-family:FreeEuro;-inkscape-font-specification:FreeEuro Bold"
+           y="519.50671"
+           x="27.886671"
+           id="tspan3559-5-4-1-5-0-6"
+           sodipodi:role="line">IOT</tspan></text>
+      <path
+         inkscape:connector-curvature="0"
+         d="m 296.73007,473.23356 c 28.21686,16.29102 28.75566,58.73779 0.99693,78.53831 -7.67688,5.47598 -8.77935,4.91028 -1.99529,-1.0238 17.47377,-15.28453 17.98492,-42.17775 1.08522,-57.09786 l -3.91266,-3.45435 0.72312,-3.71053 c 0.39771,-2.04076 0.5997,-5.73115 0.44885,-8.20083 -0.33876,-5.54623 0.15803,-6.49185 2.65383,-5.05094 z m -64.76568,11.40332 c 7.06047,-7.74198 18.64659,-14.16089 29.04027,-16.08874 l 6.87489,-1.27521 0.87404,2.89709 c 0.4807,1.59343 0.67439,5.2245 0.43037,8.06906 l -0.44364,5.17195 -6.13887,1.6918 c -10.91241,3.00731 -20.4022,10.85909 -25.4533,21.05979 l -2.41633,4.87984 -2.74281,-0.41238 c -5.14252,-0.77316 -12.72985,-3.97645 -12.79123,-5.40033 -0.092,-2.13451 8.34659,-15.74625 12.76661,-20.59287 z m 33.20546,36.39493 c -28.21687,16.29101 -65.24624,-4.46574 -68.51461,-38.40577 -0.9039,-9.38637 0.13723,-10.0583 1.88428,-1.21608 4.49989,22.77499 27.53453,36.66428 48.90556,29.48876 l 4.94788,-1.66128 2.85184,2.48149 c 1.56852,1.36481 4.66349,3.38493 6.87772,4.48914 4.97257,2.47973 5.54308,3.38282 3.04733,4.82374 z m 22.50729,-61.79039 c 3.17451,9.98553 2.94038,23.22889 -0.58688,33.19399 l -2.33309,6.59143 -2.94597,-0.69161 c -1.6203,-0.38041 -4.86173,-2.02821 -7.2032,-3.6618 l -4.25721,-2.97018 1.60429,-6.16234 c 2.85178,-10.95404 0.79685,-23.09833 -5.51167,-32.57307 l -3.01788,-4.53253 1.72854,-2.16916 c 3.24083,-4.06698 9.80863,-9.03614 11.07242,-8.37738 1.89457,0.98756 9.46336,15.1015 11.45065,21.35265 z m -48.80223,10.31437 c 0,-32.58201 36.49058,-54.27201 67.51771,-40.1325 8.58077,3.9104 8.6421,5.148 0.11108,2.23988 -21.97368,-7.49048 -45.51946,5.51348 -49.99082,27.6091 l -1.03521,5.11561 -3.57498,1.22902 c -1.96621,0.67596 -5.26316,2.34622 -7.32655,3.71171 -4.63379,3.06649 -5.70115,3.10904 -5.70115,0.22718 z m 42.25842,50.3871 c -10.23499,-2.24356 -21.58699,-9.06801 -28.45341,-17.10525 l -4.5418,-5.31622 2.07194,-2.20549 c 1.13957,-1.21302 4.18733,-3.19628 6.77282,-4.40726 l 4.70085,-2.20176 4.53458,4.47053 c 8.06061,7.94674 19.60535,12.23927 30.96496,11.51329 l 5.43422,-0.34731 1.01427,2.58154 c 1.90169,4.84014 2.92124,13.01261 1.71883,13.77769 -1.80254,1.14695 -17.80995,0.64475 -24.21726,-0.75976 z"
+         style="fill:#5a2ca0;display:inline"
+         id="path3415-4-2-2-5-0-3-7-4-4-1-5" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text3557-5-3-7-46-7-3-7"
+         y="519.50671"
+         x="317.95816"
+         style="font-size:97.09867096px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:Sans"
+         xml:space="preserve"><tspan
+           style="font-size:97.09867096px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;font-family:FreeEuro;-inkscape-font-specification:FreeEuro Bold"
+           y="519.50671"
+           x="317.95816"
+           id="tspan3559-5-4-1-90-0-2-9"
+           sodipodi:role="line">BZH</tspan></text>
+    </g>
+    <rect
+       style="fill:none;stroke:none;display:inline"
+       id="rect3098"
+       width="577.14288"
+       height="162.54224"
+       x="59.999989"
+       y="433.94821"
+       inkscape:export-filename="/home/sdx/Pictures/Logo/logo_iot_bzh_100dpi.png"
+       inkscape:export-xdpi="100.22011"
+       inkscape:export-ydpi="100.22011" />
+  </g>
+</svg>
diff --git a/afm-client/app/Frontend/images/logo/logo_iot_bzhx350.png b/afm-client/app/Frontend/images/logo/logo_iot_bzhx350.png
new file mode 100644 (file)
index 0000000..2c3b2ae
Binary files /dev/null and b/afm-client/app/Frontend/images/logo/logo_iot_bzhx350.png differ
diff --git a/afm-client/app/Frontend/images/logo/tampon-iot-bzhx450.png b/afm-client/app/Frontend/images/logo/tampon-iot-bzhx450.png
new file mode 100644 (file)
index 0000000..f365e19
Binary files /dev/null and b/afm-client/app/Frontend/images/logo/tampon-iot-bzhx450.png differ
diff --git a/afm-client/app/Frontend/images/logo/triskel_iot_bzh.png b/afm-client/app/Frontend/images/logo/triskel_iot_bzh.png
new file mode 100644 (file)
index 0000000..832dc1f
Binary files /dev/null and b/afm-client/app/Frontend/images/logo/triskel_iot_bzh.png differ
diff --git a/afm-client/app/Frontend/images/logo/triskel_iot_bzh.svg b/afm-client/app/Frontend/images/logo/triskel_iot_bzh.svg
new file mode 100644 (file)
index 0000000..096f424
--- /dev/null
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="205.71426"
+   height="197.14285"
+   id="svg4199"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="triskel_iot_bzh.svg">
+  <defs
+     id="defs4201">
+    <filter
+       color-interpolation-filters="sRGB"
+       id="filter4111"
+       inkscape:label="Drop Shadow">
+      <feFlood
+         id="feFlood4113"
+         flood-opacity="0.475"
+         flood-color="rgb(0,0,0)"
+         result="flood" />
+      <feComposite
+         id="feComposite4115"
+         in2="SourceGraphic"
+         in="flood"
+         operator="in"
+         result="composite1" />
+      <feGaussianBlur
+         id="feGaussianBlur4117"
+         stdDeviation="5"
+         result="blur" />
+      <feOffset
+         id="feOffset4119"
+         dx="8"
+         dy="8"
+         result="offset" />
+      <feComposite
+         id="feComposite4121"
+         in2="offset"
+         in="SourceGraphic"
+         operator="over"
+         result="composite2" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.35"
+     inkscape:cx="46.428557"
+     inkscape:cy="178.57144"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="500"
+     inkscape:window-height="435"
+     inkscape:window-x="1087"
+     inkscape:window-y="400"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata4204">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-328.57144,-513.79077)">
+    <path
+       id="path3415-4-2-2-5-0-3-7-4-4-1-9"
+       style="fill:#5a2ca0;display:inline;filter:url(#filter4111)"
+       d="m 470.88567,595.30412 c 28.21686,16.29102 28.75566,58.73778 0.99693,78.5383 -7.67688,5.47598 -8.77935,4.91028 -1.99529,-1.0238 17.47377,-15.28453 17.98492,-42.17774 1.08522,-57.09785 l -3.91266,-3.45435 0.72312,-3.71053 c 0.39771,-2.04076 0.5997,-5.73115 0.44885,-8.20083 -0.33876,-5.54623 0.15803,-6.49185 2.65383,-5.05094 z m -64.76568,11.40332 c 7.06047,-7.74198 18.64659,-14.16089 29.04027,-16.08874 l 6.87489,-1.27521 0.87404,2.89709 c 0.4807,1.59343 0.67439,5.2245 0.43037,8.06906 l -0.44364,5.17195 -6.13887,1.6918 c -10.91241,3.00731 -20.4022,10.85909 -25.4533,21.05979 l -2.41633,4.87984 -2.74281,-0.41238 c -5.14252,-0.77316 -12.72985,-3.97645 -12.79123,-5.40033 -0.092,-2.13451 8.34659,-15.74625 12.76661,-20.59287 z m 33.20546,36.39493 c -28.21687,16.291 -65.24624,-4.46574 -68.51461,-38.40577 -0.9039,-9.38637 0.13723,-10.0583 1.88428,-1.21608 4.49989,22.77499 27.53453,36.66428 48.90556,29.48876 l 4.94788,-1.66128 2.85184,2.48149 c 1.56852,1.36481 4.66349,3.38493 6.87772,4.48914 4.97257,2.47973 5.54308,3.38282 3.04733,4.82374 z m 22.50729,-61.7904 c 3.17451,9.98554 2.94038,23.2289 -0.58688,33.194 l -2.33309,6.59143 -2.94597,-0.69161 c -1.6203,-0.38041 -4.86173,-2.02821 -7.2032,-3.6618 l -4.25721,-2.97018 1.60429,-6.16234 c 2.85178,-10.95404 0.79685,-23.09834 -5.51167,-32.57308 l -3.01788,-4.53253 1.72854,-2.16916 c 3.24083,-4.06698 9.80863,-9.03614 11.07242,-8.37738 1.89457,0.98756 9.46336,15.1015 11.45065,21.35265 z m -48.80223,10.31438 c 0,-32.58202 36.49058,-54.27202 67.51771,-40.13251 8.58077,3.9104 8.6421,5.148 0.11108,2.23988 -21.97368,-7.49048 -45.51946,5.51348 -49.99082,27.6091 l -1.03521,5.11562 -3.57498,1.22902 c -1.96621,0.67596 -5.26316,2.34622 -7.32655,3.71171 -4.63379,3.06649 -5.70115,3.10904 -5.70115,0.22718 z m 42.25842,50.3871 c -10.23499,-2.24356 -21.58699,-9.06801 -28.45341,-17.10525 l -4.5418,-5.31622 2.07194,-2.20549 c 1.13957,-1.21302 4.18733,-3.19628 6.77282,-4.40726 l 4.70085,-2.20176 4.53458,4.47053 c 8.06061,7.94674 19.60535,12.23927 30.96496,11.51329 l 5.43422,-0.34731 1.01427,2.58154 c 1.90169,4.84014 2.92124,13.01261 1.71883,13.77769 -1.80254,1.14695 -17.80995,0.64475 -24.21726,-0.75976 z"
+       inkscape:connector-curvature="0"
+       inkscape:export-filename="/home/sdx/Pictures/Logo/triskel_iot_bzh_300dpi.png"
+       inkscape:export-xdpi="300"
+       inkscape:export-ydpi="300" />
+    <rect
+       style="fill:none;stroke:none;display:inline"
+       id="rect4179"
+       width="205.71426"
+       height="197.14285"
+       x="328.57144"
+       y="513.79077"
+       inkscape:export-filename="/home/sdx/Pictures/Logo/triskel_iot_bzh_300dpi.png"
+       inkscape:export-xdpi="300"
+       inkscape:export-ydpi="300" />
+  </g>
+</svg>
diff --git a/afm-client/app/Frontend/images/logo/triskel_iot_bzhx250.png b/afm-client/app/Frontend/images/logo/triskel_iot_bzhx250.png
new file mode 100644 (file)
index 0000000..f4e41ae
Binary files /dev/null and b/afm-client/app/Frontend/images/logo/triskel_iot_bzhx250.png differ
diff --git a/afm-client/app/Frontend/index.html b/afm-client/app/Frontend/index.html
new file mode 100644 (file)
index 0000000..0d55267
--- /dev/null
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--[if lt IE 7]>      <html lang="en" ng-app="@@APPNAME@@" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
+<!--[if IE 7]>         <html lang="en" ng-app="@@APPNAME@@" class="no-js lt-ie9 lt-ie8"> <![endif]-->
+<!--[if IE 8]>         <html lang="en" ng-app="@@APPNAME@@" class="no-js lt-ie9"> <![endif]-->
+<!--[if gt IE 8]><!--> <html lang="en" ng-app="@@APPNAME@@" class="no-js"> <!--<![endif]-->
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>Simple Sample Application</title>
+    <meta name="description" content="">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <!-- bower:css -->
+    <!-- endinject -->
+     <!--vendor:css -->
+    <!-- endinject -->
+    <!-- appli:css -->
+    <!-- endinject -->
+    <!-- inject:css -->
+    <!-- endinject -->
+    <base href="@@URLBASE@@"> <!-- https://docs.angularjs.org/error/$location/nobase -->
+
+</head>
+<body>
+<!--[if lt IE 7]>
+<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
+<![endif]-->
+<div ui-view></div>
+<!-- bower:js -->
+<!-- endinject -->
+<!-- inject:js -->
+<!-- endinject -->
+
+<!-- Generic Foundation Modal Template -->
+<script id="components/modal/modal.html" type="text/ng-template">
+   <div  class="modal-overlay" ng-click="hideOverlay()">
+    <aside class="modal" ng-click="$event.stopPropagation();" ng-transclude></aside>
+  </div>
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/afm-client/app/Frontend/pages/Dashboard/Dashboard.html b/afm-client/app/Frontend/pages/Dashboard/Dashboard.html
new file mode 100644 (file)
index 0000000..c393271
--- /dev/null
@@ -0,0 +1,33 @@
+<!-- Foundation Annotations generate tmp/route.js -->
+---
+name: Dashboard
+url:  /dashboard
+controller:   DashboardController as ctrl
+animationIn: slideInRight
+---
+
+<h3> 
+<img class="logo" src="images/logo/triskel_iot_bzhx250.png" alt="IoT.bzh Logo" style="height:150px;">
+  Application Framework Manager Simple Client
+</h3>
+
+
+
+<div class="button-box box-content ">
+    <appli-button  ng-repeat="appliID in ctrl.appliIDs" store="ctrl.appliStore" handle="{{appliID}}" callback="ctrl.AppliCB"> </appli-button>
+    <upload-appli  class="ibz-right" label="Upload" icon="fi-upload" posturl="/api/post/upload-appli" callback="ctrl.FileUploaded" title="Upload AGL App"></upload-appli>
+</div>
+
+
+<div class="message-box box-content vertical grid-frame">
+    <div class="response">
+        <span class="grid-content noscroll req {{ctrl.status}} ">req= {{ctrl.request}}</span>
+        <span class="grid-content noscroll res {{ctrl.status}} ">res= {{ctrl.response}}</span>
+        <span class="grid-content noscroll status {{ctrl.status}}">status= {{ctrl.errcode}}</span>
+    </div>
+</div>
+
+
+<link-button href="sample" icon="fi-home" label="sample"></link-button>
+<token-refresh autolog="true" callback="ctrl.AutoStart"></token-refresh>
+
diff --git a/afm-client/app/Frontend/pages/Dashboard/DashboardModule.js b/afm-client/app/Frontend/pages/Dashboard/DashboardModule.js
new file mode 100644 (file)
index 0000000..c075372
--- /dev/null
@@ -0,0 +1,75 @@
+(function() {
+'use strict';
+
+// WARNING: make sure than app/frontend/services/AppConfig.js match your server
+
+// list all rependencies within the page + controler if needed
+angular.module('DashboardModule', ['SubmitButton', 'TokenRefresh', 'AppliButton'])
+
+  .controller('DashboardController', function (AppCall, Notification) {
+        var scope = this; // I hate JavaScript
+        scope.uuid   ="none";
+        scope.token  ="none";
+        scope.session="none";
+        scope.status ="err-no";
+        scope.appliIDs =[]; // array to hold applications ID
+        scope.appliStore={}; // array to hold applications json description
+
+        scope.AppliCB = function (appliID) {
+            console.log ("Application Clicked ID=[%s]", appliID);
+            
+        };
+        
+        scope.AppliCB = function(appliID, action, response) {
+                // Action is done within Widget Controller only update debug UI zone
+                scope.request  = action; 
+                scope.errcode  = response.status;
+                if (response.data) scope.response = response.data;
+        };
+        
+        scope.GetRunnables = function() {
+            console.log ("Dashboard GetRunnables");
+            
+            AppCall.get ("afm-main", "runnables", {/*query*/}, function(response) {
+                
+                // update debug UI zone
+                scope.request  = "/api/afm-main/runnable"; 
+                scope.response = response.data;
+                scope.errcode  = response.status;
+                
+                if (response.status !== 200) {
+                    console.log ("Hoop GetRunnable failed");
+                    return;
+                }
+                
+                // Check this is a valid response from Binder
+                if (response.data.request.jtype !== "AJB_reply" && response.data.request.api !== "runnables") {
+                  Notification.error ({message: "Invalid Respond to /opa/afm-main/runnable response.data="+response.data, delay: 5000}); 
+                  return;
+                }
+                
+                // loop on runnable application to prepare for display
+                var  appliIDs=[];
+                for (var idx=0; idx < response.data.response.length; idx ++) {
+                    appliIDs[idx] = response.data.response [idx].id;
+                    scope.appliStore [response.data.response [idx].id] =  response.data.response [idx];
+                }
+                scope.appliIDs = appliIDs; // avoid partial update to limit UI refresh
+                
+            });            
+        };
+        
+        scope.FileUploaded = function (status) {
+            console.log ("file Uploaded");
+            scope.GetRunnables();
+        };
+        
+        scope.AutoStart = function () {
+            console.log ("AutoStart requesting Apps list");
+            scope.GetRunnables();
+        };
+          
+   });
+
+console.log ("Dashboard Controller Loaded");
+})(); 
\ No newline at end of file
diff --git a/afm-client/app/Frontend/pages/Dashboard/DashboardModule.scss b/afm-client/app/Frontend/pages/Dashboard/DashboardModule.scss
new file mode 100644 (file)
index 0000000..8bf04a1
--- /dev/null
@@ -0,0 +1,71 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "app/ibz-mixins";
+
+$COLOR_SUCCESS: green;
+$COLOR_FAIL: red;
+
+token-refresh {
+    display: block;
+    float: right;
+    margin: .5rem 1rem 0 0;
+}
+
+.button-box {
+    height  : 4.5rem;
+    
+    .session-button {
+      float: left;
+      width: 5rem;
+    };
+    
+    .response > span{
+        display: block;
+        margin: .3rem .5rem .3rem .5rem;        
+    }  
+    
+    .fail { 
+        color:$COLOR_FAIL;
+        border: 1px solid darken($COLOR_FAIL,10%);
+    }
+    .success { 
+        color:$COLOR_SUCCESS; 
+        border: 1px solid darken($COLOR_SUCCESS,10%);
+    }
+
+};
+
+.message-box {
+    height  : auto;
+    width: 100%;
+    font-size: .75rem;
+  
+    .response {
+        .err-no { color:grey; }
+        .res.err-ok { color: blue; }
+        .req.err-ok { color:blueviolet; }
+        .status.err-ok { color:green; }
+        .status.err-fx { color:red; }       
+    } 
+    
+
+};
+
+
+
diff --git a/afm-client/app/Frontend/pages/Home/Dashboard.html b/afm-client/app/Frontend/pages/Home/Dashboard.html
new file mode 100644 (file)
index 0000000..036148a
--- /dev/null
@@ -0,0 +1,40 @@
+<!-- Foundation Annotations generate tmp/route.js -->
+---
+name: dashboard
+url:  /dashboard
+controller:   DashboardController as ctrl
+animationIn: slideInRight
+---
+
+<h3> 
+<img class="logo" src="images/logo/triskel_iot_bzhx250.png" alt="IoT.bzh Logo" style="height:150px;">
+  Application Framework Manager Simple Client
+</h3>
+
+
+
+<div class="button-box box-content ">
+    <appli-button  ng-repeat="appliID in ctrl.appliIDs" store="ctrl.appliStore" handle={{appliID}} callback="ctrl.AppliCB"> </appli-button>
+    <upload-appli  class="ibz-right" label="Upload" icon="fi-upload" posturl="/api/post/upload-appli" callback="ctrl.FileUploaded" title="Upload AGL App"></upload-appli>
+</div>
+
+
+<div class="button-box box-content ">
+      
+    <submit-button class="session-button {{ctrl.APIcheck}}" icon="fi-play-circle" label="Start" clicked="ctrl.StartApp" ></submit-button>
+    <submit-button class="session-button {{ctrl.APIrefresh}}"  icon="fi-x-circle" label="Stop"   clicked="ctrl.StopApp" ></submit-button>
+    <submit-button class="session-button {{ctrl.APIreset}}" icon="fi-x" label="Remove" clicked="ctrl.RemoveApp" ></submit-button>
+    
+</div>
+<div class="message-box box-content vertical grid-frame">
+    <div class="response">
+        <span class="grid-content noscroll req {{ctrl.status}} ">req= {{ctrl.request}}</span>
+        <span class="grid-content noscroll res {{ctrl.status}} ">res= {{ctrl.response}}</span>
+        <span class="grid-content noscroll status {{ctrl.status}}">status= {{ctrl.errcode}}</span>
+    </div>
+</div>
+
+
+<link-button href="sample" icon="fi-home" label="sample"></link-button>
+<token-refresh autolog="true" callback="ctrl.AutoStart"></token-refresh>
+
diff --git a/afm-client/app/Frontend/pages/Home/DashboardModule.js b/afm-client/app/Frontend/pages/Home/DashboardModule.js
new file mode 100644 (file)
index 0000000..f843df1
--- /dev/null
@@ -0,0 +1,118 @@
+(function() {
+'use strict';
+
+// WARNING: make sure than app/frontend/services/AppConfig.js match your server
+
+// list all rependencies within the page + controler if needed
+angular.module('DashboardModule', ['SubmitButton', 'TokenRefresh', 'AppliButton'])
+
+  .controller('DashboardController', function ($http, AppConfig, Notification) {
+        var scope = this; // I hate JavaScript
+        scope.uuid   ="none";
+        scope.token  ="none";
+        scope.session="none";
+        scope.status ="err-no";
+        scope.appliIDs =[]; // array to hold applications ID
+        scope.appliStore={}; // array to hold applications json description
+
+        scope.ProcessResponse= function(data, errcode, headers, config) {
+            var apiname= 'API'+ data.request.api.replace('-','_');
+            scope.status = "err-ok";
+            scope.errcode= errcode;
+            scope.request  = data.request;
+            scope.response = data.response;
+            
+            // if token was refresh let's update AppConfig
+            if (data.request.token) AppConfig.session.token = data.request.token;
+            if (data.request.uuid)  AppConfig.session.uuid  = data.request.uuid;
+            if (data.request.timeout)  AppConfig.session.timeout  = data.request.timeout;
+
+            // Make sure we clean everything when Open/Close is called
+            if (apiname === "APIcreate" || apiname === "APIreset") {
+                scope.APIreset  ='';
+                scope.APIcreate ='';
+                scope.APIrefresh='';
+                scope.APIcheck  ='';
+            }
+            scope[apiname]="success";
+            
+            // If we have a new token let's update it
+            if (data.request.token) scope.token=data.request.token;
+            
+            console.log ("OK: "+ JSON.stringify(data));
+        };
+        
+        scope.ProcessError= function(data, errcode, headers, config) {
+            var apiname= 'API'+data.request.api.replace('-','_');
+            scope.status   = "err-fx";
+            scope.errcode  = errcode;
+            scope.request  = data.request;
+            scope.response = "";
+            scope[apiname]="fail";
+            
+            console.log ("FX: "+ JSON.stringify(data));
+        };
+
+        scope.OpenSession = function() {
+            console.log ("OpenSession"); 
+            var handler = $http.get(AppConfig.session.create + '?token='+AppConfig.session.initial);
+            
+            handler.success(scope.ProcessResponse);
+            handler.error(scope.ProcessError);
+        };        
+
+        scope.CheckSession = function() {
+            console.log ("CloseSession");
+            var handler = $http.get(AppConfig.session.check + '?token='+AppConfig.session.token);
+            
+            handler.success(scope.ProcessResponse);
+            handler.error(scope.ProcessError);
+        };
+        
+        scope.RefreshSession = function() {
+            console.log ("RefreshSession");
+            var handler = $http.get(AppConfig.session.refresh + '?token='+AppConfig.session.token);
+            
+            handler.success(scope.ProcessResponse);
+            handler.error(scope.ProcessError);
+        };
+        
+        scope.ResetSession = function() {
+            console.log ("ResetSession");
+            var handler = $http.get(AppConfig.session.reset + '?token='+AppConfig.session.token);
+            
+            handler.success(scope.ProcessResponse);
+            handler.error(scope.ProcessError);
+        };
+        
+        scope.AppliCB = function (appliID) {
+            console.log ("Application Clicked ID=[%s]", appliID);
+            
+        };
+        
+        scope.AutoStart = function () {
+            console.log ("AutoStart requesting Apps list");
+            var handler = $http.get('/api/afm-main/runnables' + '?token='+AppConfig.session.token);
+            handler.success(function(result) {
+                
+                // Check this is a valid response from Binder
+                if (result.request.jtype !== "AJB_reply" && result.request.api !== "runnables") {
+                  Notification.error ({message: "Invalid Respond to /opa/afm-main/runnable result="+result, delay: 5000}); 
+                  return;
+                }
+                
+                // loop on runnable application to prepare for display
+                var  appliIDs=[];
+                for (var idx=0; idx < result.response.length; idx ++) {
+                    appliIDs[idx] = result.response [idx].id;
+                    scope.appliStore [result.response [idx].id] =  result.response [idx];
+                }
+                scope.appliIDs = appliIDs; // avoid partial update to limit UI refresh
+                
+            });
+        };
+          
+   });
+
+console.log ("Dashboard Controller Loaded");
+})(); 
\ No newline at end of file
diff --git a/afm-client/app/Frontend/pages/Home/DashboardModule.scss b/afm-client/app/Frontend/pages/Home/DashboardModule.scss
new file mode 100644 (file)
index 0000000..8bf04a1
--- /dev/null
@@ -0,0 +1,71 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "app/ibz-mixins";
+
+$COLOR_SUCCESS: green;
+$COLOR_FAIL: red;
+
+token-refresh {
+    display: block;
+    float: right;
+    margin: .5rem 1rem 0 0;
+}
+
+.button-box {
+    height  : 4.5rem;
+    
+    .session-button {
+      float: left;
+      width: 5rem;
+    };
+    
+    .response > span{
+        display: block;
+        margin: .3rem .5rem .3rem .5rem;        
+    }  
+    
+    .fail { 
+        color:$COLOR_FAIL;
+        border: 1px solid darken($COLOR_FAIL,10%);
+    }
+    .success { 
+        color:$COLOR_SUCCESS; 
+        border: 1px solid darken($COLOR_SUCCESS,10%);
+    }
+
+};
+
+.message-box {
+    height  : auto;
+    width: 100%;
+    font-size: .75rem;
+  
+    .response {
+        .err-no { color:grey; }
+        .res.err-ok { color: blue; }
+        .req.err-ok { color:blueviolet; }
+        .status.err-ok { color:green; }
+        .status.err-fx { color:red; }       
+    } 
+    
+
+};
+
+
+
diff --git a/afm-client/app/Frontend/pages/Sample/Sample.html b/afm-client/app/Frontend/pages/Sample/Sample.html
new file mode 100644 (file)
index 0000000..03a4558
--- /dev/null
@@ -0,0 +1,35 @@
+<!-- Foundation Annotations generate tmp/route.js -->
+---
+name: mysample
+url:  /sample
+controller:   SampleController as ctrl
+animationIn: slideInRight
+---
+
+<h1><img class="logo" src="images/logo/triskel_iot_bzhx250.png" alt="IoT.bzh Logo" style="height:150px;">
+    Post File Upload
+</h1>
+
+<div class="sample-box box-content">
+    
+    <!-- Usage: upload-xxxxx
+         name     = [xxxxxx] is use a field label for xform input field. Should match with server side
+         category = [avatar] should match to a valid directory of thumbnail within AppConfig.path
+         thumbnail= [tux-bzh.png] a valid image within AppConfig.paths.[category]
+         istoobig = [istoobig.png] used image from AppConfig.paths.[category] when file is oversized
+         maxsize  = [xxx] maximum size in KB [default max depend on upload-type]
+         accept = [image] acceptable accept for upload
+    -->
+    <upload-image name="avatar" category="avatar" thumbnail="tux-visitor.png" maxsize="100" 
+        posturl="/api/post/upload-image" callback="ctrl.FileUploaded" accept="image" title="Change your Avatar">
+    </upload-image>
+    
+    <!-- Warning: name=xxx should match with what server expect [used as xform input name -->
+    <upload-audio  name="music" posturl="/api/post/upload-music" callback="ctrl.FileUploaded" title="Upload your Music"></upload-audio>
+
+    <!-- Warning: name=xxx should match with what server expect [used as xform input name -->
+    <upload-appli  name="appli" posturl="/api/post/upload-appli" callback="ctrl.FileUploaded" title="Upload AGL App"></upload-appli>
+
+</div>
+
+<link-button href="home" icon="fi-home" label="home"></link-button>
diff --git a/afm-client/app/Frontend/pages/Sample/SampleModule.js b/afm-client/app/Frontend/pages/Sample/SampleModule.js
new file mode 100644 (file)
index 0000000..8ae82ea
--- /dev/null
@@ -0,0 +1,18 @@
+(function() {
+'use strict';
+
+// list all rependencies within the page + controler if needed
+angular.module('SampleModule', ['SubmitButton','UploadFiles'])
+
+  .controller('SampleController', function ($http) {
+        var scope = this; // I hate JavaScript
+
+        console.log ("sample Init");
+        
+        scope.FileUploaded = function (response) {
+           console.log ("FileUploaded response=%s", JSON.stringify(response));
+        };
+   });
+
+console.log ("SampleControler Loaded");
+})(); 
\ No newline at end of file
diff --git a/afm-client/app/Frontend/pages/Sample/SampleModule.scss b/afm-client/app/Frontend/pages/Sample/SampleModule.scss
new file mode 100644 (file)
index 0000000..7654424
--- /dev/null
@@ -0,0 +1,41 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "app/ibz-mixins";
+
+
+.sample-box {
+    display: block;
+    height  : 4.5rem;
+    
+    .sample-button {
+      float: right;
+      width: 5rem;
+    };
+    
+    .muted-on-on,.muted-off-off{
+        background: blueviolet;
+    };
+        
+    .muted-error{
+        background: red;
+    };
+};
+
+
+
diff --git a/afm-client/app/Frontend/services/JQueryEmu.js b/afm-client/app/Frontend/services/JQueryEmu.js
new file mode 100644 (file)
index 0000000..6d6e338
--- /dev/null
@@ -0,0 +1,79 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ * 
+ * Usage:
+ * 
+ *   // mouse event probably point on icon and not on button div
+ *   ... ng-click="LockChannel($event)
+     var target= angular.element(event.currentTarget);
+     var button= JQemu.FindInParent (target, 'div');
+ * 
+ */
+
+
+(function () {
+    'use strict';
+
+
+          
+    // _all modules only reference dependencies
+    angular.module('JQueryEmu', [])
+
+            // Factory is a singleton and share its context within all instances.
+            .factory('JQemu', function () {
+
+                var FindInParent = function (element, selector) {
+                    var parent = element;
+                    var search = selector.toUpperCase();
+                    while (parent[0]) {
+                        if (search === parent[0].tagName) {
+                            return parent;
+                        }  // HTMLDivElement properties
+                        parent = parent.parent();
+                    }
+                };
+                
+                var  FindByTag= function (element, tag, selector) {
+                    var search = selector.toLowerCase();
+                    var type   = tag.toLowerCase()+ "Name";
+                    var children = element.children();
+                    while (children[0]) {
+                        if (search === children[0][type]) {
+                            return children;
+                        }  // HTMLDivElement properties
+                        children = children.next();
+                    }
+                };
+                
+                var  FindByClass= function (element, selector) {
+                    var search = selector.toLowerCase();
+                    var children = element.children();
+                    while (children[0]) {
+                        if (children.hasClass(search)) {
+                            return children;
+                        }  // HTMLDivElement properties
+                        children = children.next();
+                    }
+                };
+
+                var myMethods = {
+                    FindInParent: FindInParent,
+                    FindByTag: FindByTag,
+                    FindByClass: FindByClass
+                };
+
+                return myMethods;
+            });
+
+})();
\ No newline at end of file
diff --git a/afm-client/app/Frontend/styles/README.md b/afm-client/app/Frontend/styles/README.md
new file mode 100644 (file)
index 0000000..dc50ced
--- /dev/null
@@ -0,0 +1,28 @@
+WARNING note about global style dir
+-------------------------------------
+
+ - styles placed in Frontend/styles is global and will be posted in dist.prod/styles
+ - styles defined within widget or page directory will be place in dist.prof/opa/styles
+
+This model allows to share global styles by multiple applications.
+
+To change this behaviour just rename styles directory on something else [eg: appstyles]
+
+
+    |---- /Frontend
+    |     |
+    |     |---- /styles
+    |     |     |
+    |     |     |---- _settings.scss
+    |     |     |---- app.scss
+    |     |
+    |     |---- /Widgets
+    |     |     |
+    |     |     |--- widget.js
+    |     |     |--- widget.sccs
+    |     |
+    |     |-----/Pages
+    |           |--- page-partial.html
+    |           |--- page-cntrl.js
+    |           |--- page-style.scss 
+    |
diff --git a/afm-client/app/Frontend/styles/app/_ibz-mixins.scss b/afm-client/app/Frontend/styles/app/_ibz-mixins.scss
new file mode 100644 (file)
index 0000000..ed9dba8
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+    Every SCSS files will be injected into main HTML page
+*/
+%shadow-transition {
+    transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s;
+    border-radius: 5px;
+}
+
+@mixin ibz-box-content {
+    border: 2px solid gainsboro !important;
+    border-radius: 5px;
+    
+    margin-left: auto;
+    margin-right: auto;
+    min-width: 98% !important; 
+    margin: .5rem .5rem .5rem .5rem;
+    background: rgba(200,205,200,.3);
+}
+
+@mixin ibz-button ($color, $size:2rem) {
+    display: inline-block;
+    border: 1px solid darken($color,10%);
+    box-shadow: 2px 2px 1px adjust-hue($color,20deg);
+    color: $color;
+    border-radius: 5px;
+    font-style: italic;
+    padding: 0rem .5rem 0rem .5rem;
+    margin:.5rem;
+    background: lighten(#9494b7, 20%);
+    i {
+        padding-right: 0.3rem;
+        font-size: $size;
+    }
+    span {font-size: 1rem}
+    
+    &:hover {
+      background: lighten($color, 45%);
+      border: 2px solid darken($color,20%);
+      
+    }
+}
+
+@mixin ibz-input-alert ($color, $background) {
+    position  :fixed;
+    margin: -2.2rem 0 0 15rem;
+    border-radius: 5px;
+    font-style: italic;
+    border-color: darken($background,10%);
+    color: $color;
+    background-color: $background !important;
+    padding: 0.2rem !important;
+}
diff --git a/afm-client/app/Frontend/styles/app/ibz-global.scss b/afm-client/app/Frontend/styles/app/ibz-global.scss
new file mode 100644 (file)
index 0000000..e28e7be
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+Copyright (C) 2015 "IoT.bzh"
+Author "Fulup Ar Foll"
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import "app/ibz-mixins";
+
+submit-button {
+    float: right;
+    @include ibz-button(grey);
+}
+
+zf-modal {
+  background-color: rgba(180,180,180,.5);
+}
+
+.box-content {
+     @include ibz-box-content;
+
+    .box-title {
+        display: block;
+        font-size: 1.3rem;
+    }
+}
+
+// Change Notifications default size
+.ui-notification {
+    width: auto;
+    border-radius: 5px;
+}
+
+.ibz-right {
+    float: right !important;
+}
+
+.ibz-left {
+    float: left !important;
+}
\ No newline at end of file
diff --git a/afm-client/app/Frontend/styles/foundation/_foundation-icons.scss b/afm-client/app/Frontend/styles/foundation/_foundation-icons.scss
new file mode 100644 (file)
index 0000000..d401f3c
--- /dev/null
@@ -0,0 +1,591 @@
+/* 
+ * Foundation Icons v 3.0
+ * Made by ZURB 2013 http://zurb.com/playground/foundation-icon-fonts-3
+ * MIT License
+ */
+
+@font-face {
+  font-family: "foundation-icons";
+  src: url("/bower_components/foundation-icons.woff") format("woff");
+  font-weight: normal;
+  font-style: normal;
+}
+
+
+.fi-address-book:before,
+.fi-alert:before,
+.fi-align-center:before,
+.fi-align-justify:before,
+.fi-align-left:before,
+.fi-align-right:before,
+.fi-anchor:before,
+.fi-annotate:before,
+.fi-archive:before,
+.fi-arrow-down:before,
+.fi-arrow-left:before,
+.fi-arrow-right:before,
+.fi-arrow-up:before,
+.fi-arrows-compress:before,
+.fi-arrows-expand:before,
+.fi-arrows-in:before,
+.fi-arrows-out:before,
+.fi-asl:before,
+.fi-asterisk:before,
+.fi-at-sign:before,
+.fi-background-color:before,
+.fi-battery-empty:before,
+.fi-battery-full:before,
+.fi-battery-half:before,
+.fi-bitcoin-circle:before,
+.fi-bitcoin:before,
+.fi-blind:before,
+.fi-bluetooth:before,
+.fi-bold:before,
+.fi-book-bookmark:before,
+.fi-book:before,
+.fi-bookmark:before,
+.fi-braille:before,
+.fi-burst-new:before,
+.fi-burst-sale:before,
+.fi-burst:before,
+.fi-calendar:before,
+.fi-camera:before,
+.fi-check:before,
+.fi-checkbox:before,
+.fi-clipboard-notes:before,
+.fi-clipboard-pencil:before,
+.fi-clipboard:before,
+.fi-clock:before,
+.fi-closed-caption:before,
+.fi-cloud:before,
+.fi-comment-minus:before,
+.fi-comment-quotes:before,
+.fi-comment-video:before,
+.fi-comment:before,
+.fi-comments:before,
+.fi-compass:before,
+.fi-contrast:before,
+.fi-credit-card:before,
+.fi-crop:before,
+.fi-crown:before,
+.fi-css3:before,
+.fi-database:before,
+.fi-die-five:before,
+.fi-die-four:before,
+.fi-die-one:before,
+.fi-die-six:before,
+.fi-die-three:before,
+.fi-die-two:before,
+.fi-dislike:before,
+.fi-dollar-bill:before,
+.fi-dollar:before,
+.fi-download:before,
+.fi-eject:before,
+.fi-elevator:before,
+.fi-euro:before,
+.fi-eye:before,
+.fi-fast-forward:before,
+.fi-female-symbol:before,
+.fi-female:before,
+.fi-filter:before,
+.fi-first-aid:before,
+.fi-flag:before,
+.fi-folder-add:before,
+.fi-folder-lock:before,
+.fi-folder:before,
+.fi-foot:before,
+.fi-foundation:before,
+.fi-graph-bar:before,
+.fi-graph-horizontal:before,
+.fi-graph-pie:before,
+.fi-graph-trend:before,
+.fi-guide-dog:before,
+.fi-hearing-aid:before,
+.fi-heart:before,
+.fi-home:before,
+.fi-html5:before,
+.fi-indent-less:before,
+.fi-indent-more:before,
+.fi-info:before,
+.fi-italic:before,
+.fi-key:before,
+.fi-laptop:before,
+.fi-layout:before,
+.fi-lightbulb:before,
+.fi-like:before,
+.fi-link:before,
+.fi-list-bullet:before,
+.fi-list-number:before,
+.fi-list-thumbnails:before,
+.fi-list:before,
+.fi-lock:before,
+.fi-loop:before,
+.fi-magnifying-glass:before,
+.fi-mail:before,
+.fi-male-female:before,
+.fi-male-symbol:before,
+.fi-male:before,
+.fi-map:before,
+.fi-marker:before,
+.fi-megaphone:before,
+.fi-microphone:before,
+.fi-minus-circle:before,
+.fi-minus:before,
+.fi-mobile-signal:before,
+.fi-mobile:before,
+.fi-monitor:before,
+.fi-mountains:before,
+.fi-music:before,
+.fi-next:before,
+.fi-no-dogs:before,
+.fi-no-smoking:before,
+.fi-page-add:before,
+.fi-page-copy:before,
+.fi-page-csv:before,
+.fi-page-delete:before,
+.fi-page-doc:before,
+.fi-page-edit:before,
+.fi-page-export-csv:before,
+.fi-page-export-doc:before,
+.fi-page-export-pdf:before,
+.fi-page-export:before,
+.fi-page-filled:before,
+.fi-page-multiple:before,
+.fi-page-pdf:before,
+.fi-page-remove:before,
+.fi-page-search:before,
+.fi-page:before,
+.fi-paint-bucket:before,
+.fi-paperclip:before,
+.fi-pause:before,
+.fi-paw:before,
+.fi-paypal:before,
+.fi-pencil:before,
+.fi-photo:before,
+.fi-play-circle:before,
+.fi-play-video:before,
+.fi-play:before,
+.fi-plus:before,
+.fi-pound:before,
+.fi-power:before,
+.fi-previous:before,
+.fi-price-tag:before,
+.fi-pricetag-multiple:before,
+.fi-print:before,
+.fi-prohibited:before,
+.fi-projection-screen:before,
+.fi-puzzle:before,
+.fi-quote:before,
+.fi-record:before,
+.fi-refresh:before,
+.fi-results-demographics:before,
+.fi-results:before,
+.fi-rewind-ten:before,
+.fi-rewind:before,
+.fi-rss:before,
+.fi-safety-cone:before,
+.fi-save:before,
+.fi-share:before,
+.fi-sheriff-badge:before,
+.fi-shield:before,
+.fi-shopping-bag:before,
+.fi-shopping-cart:before,
+.fi-shuffle:before,
+.fi-skull:before,
+.fi-social-500px:before,
+.fi-social-adobe:before,
+.fi-social-amazon:before,
+.fi-social-android:before,
+.fi-social-apple:before,
+.fi-social-behance:before,
+.fi-social-bing:before,
+.fi-social-blogger:before,
+.fi-social-delicious:before,
+.fi-social-designer-news:before,
+.fi-social-deviant-art:before,
+.fi-social-digg:before,
+.fi-social-dribbble:before,
+.fi-social-drive:before,
+.fi-social-dropbox:before,
+.fi-social-evernote:before,
+.fi-social-facebook:before,
+.fi-social-flickr:before,
+.fi-social-forrst:before,
+.fi-social-foursquare:before,
+.fi-social-game-center:before,
+.fi-social-github:before,
+.fi-social-google-plus:before,
+.fi-social-hacker-news:before,
+.fi-social-hi5:before,
+.fi-social-instagram:before,
+.fi-social-joomla:before,
+.fi-social-lastfm:before,
+.fi-social-linkedin:before,
+.fi-social-medium:before,
+.fi-social-myspace:before,
+.fi-social-orkut:before,
+.fi-social-path:before,
+.fi-social-picasa:before,
+.fi-social-pinterest:before,
+.fi-social-rdio:before,
+.fi-social-reddit:before,
+.fi-social-skillshare:before,
+.fi-social-skype:before,
+.fi-social-smashing-mag:before,
+.fi-social-snapchat:before,
+.fi-social-spotify:before,
+.fi-social-squidoo:before,
+.fi-social-stack-overflow:before,
+.fi-social-steam:before,
+.fi-social-stumbleupon:before,
+.fi-social-treehouse:before,
+.fi-social-tumblr:before,
+.fi-social-twitter:before,
+.fi-social-vimeo:before,
+.fi-social-windows:before,
+.fi-social-xbox:before,
+.fi-social-yahoo:before,
+.fi-social-yelp:before,
+.fi-social-youtube:before,
+.fi-social-zerply:before,
+.fi-social-zurb:before,
+.fi-sound:before,
+.fi-star:before,
+.fi-stop:before,
+.fi-strikethrough:before,
+.fi-subscript:before,
+.fi-superscript:before,
+.fi-tablet-landscape:before,
+.fi-tablet-portrait:before,
+.fi-target-two:before,
+.fi-target:before,
+.fi-telephone-accessible:before,
+.fi-telephone:before,
+.fi-text-color:before,
+.fi-thumbnails:before,
+.fi-ticket:before,
+.fi-torso-business:before,
+.fi-torso-female:before,
+.fi-torso:before,
+.fi-torsos-all-female:before,
+.fi-torsos-all:before,
+.fi-torsos-female-male:before,
+.fi-torsos-male-female:before,
+.fi-torsos:before,
+.fi-trash:before,
+.fi-trees:before,
+.fi-trophy:before,
+.fi-underline:before,
+.fi-universal-access:before,
+.fi-unlink:before,
+.fi-unlock:before,
+.fi-upload-cloud:before,
+.fi-upload:before,
+.fi-usb:before,
+.fi-video:before,
+.fi-volume-none:before,
+.fi-volume-strike:before,
+.fi-volume:before,
+.fi-web:before,
+.fi-wheelchair:before,
+.fi-widget:before,
+.fi-wrench:before,
+.fi-x-circle:before,
+.fi-x:before,
+.fi-yen:before,
+.fi-zoom-in:before,
+.fi-zoom-out:before {
+  font-family: "foundation-icons";
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+  -webkit-font-smoothing: antialiased;
+  display: inline-block;
+  text-decoration: inherit;
+}
+
+.fi-address-book:before { content: "\f100"; }
+.fi-alert:before { content: "\f101"; }
+.fi-align-center:before { content: "\f102"; }
+.fi-align-justify:before { content: "\f103"; }
+.fi-align-left:before { content: "\f104"; }
+.fi-align-right:before { content: "\f105"; }
+.fi-anchor:before { content: "\f106"; }
+.fi-annotate:before { content: "\f107"; }
+.fi-archive:before { content: "\f108"; }
+.fi-arrow-down:before { content: "\f109"; }
+.fi-arrow-left:before { content: "\f10a"; }
+.fi-arrow-right:before { content: "\f10b"; }
+.fi-arrow-up:before { content: "\f10c"; }
+.fi-arrows-compress:before { content: "\f10d"; }
+.fi-arrows-expand:before { content: "\f10e"; }
+.fi-arrows-in:before { content: "\f10f"; }
+.fi-arrows-out:before { content: "\f110"; }
+.fi-asl:before { content: "\f111"; }
+.fi-asterisk:before { content: "\f112"; }
+.fi-at-sign:before { content: "\f113"; }
+.fi-background-color:before { content: "\f114"; }
+.fi-battery-empty:before { content: "\f115"; }
+.fi-battery-full:before { content: "\f116"; }
+.fi-battery-half:before { content: "\f117"; }
+.fi-bitcoin-circle:before { content: "\f118"; }
+.fi-bitcoin:before { content: "\f119"; }
+.fi-blind:before { content: "\f11a"; }
+.fi-bluetooth:before { content: "\f11b"; }
+.fi-bold:before { content: "\f11c"; }
+.fi-book-bookmark:before { content: "\f11d"; }
+.fi-book:before { content: "\f11e"; }
+.fi-bookmark:before { content: "\f11f"; }
+.fi-braille:before { content: "\f120"; }
+.fi-burst-new:before { content: "\f121"; }
+.fi-burst-sale:before { content: "\f122"; }
+.fi-burst:before { content: "\f123"; }
+.fi-calendar:before { content: "\f124"; }
+.fi-camera:before { content: "\f125"; }
+.fi-check:before { content: "\f126"; }
+.fi-checkbox:before { content: "\f127"; }
+.fi-clipboard-notes:before { content: "\f128"; }
+.fi-clipboard-pencil:before { content: "\f129"; }
+.fi-clipboard:before { content: "\f12a"; }
+.fi-clock:before { content: "\f12b"; }
+.fi-closed-caption:before { content: "\f12c"; }
+.fi-cloud:before { content: "\f12d"; }
+.fi-comment-minus:before { content: "\f12e"; }
+.fi-comment-quotes:before { content: "\f12f"; }
+.fi-comment-video:before { content: "\f130"; }
+.fi-comment:before { content: "\f131"; }
+.fi-comments:before { content: "\f132"; }
+.fi-compass:before { content: "\f133"; }
+.fi-contrast:before { content: "\f134"; }
+.fi-credit-card:before { content: "\f135"; }
+.fi-crop:before { content: "\f136"; }
+.fi-crown:before { content: "\f137"; }
+.fi-css3:before { content: "\f138"; }
+.fi-database:before { content: "\f139"; }
+.fi-die-five:before { content: "\f13a"; }
+.fi-die-four:before { content: "\f13b"; }
+.fi-die-one:before { content: "\f13c"; }
+.fi-die-six:before { content: "\f13d"; }
+.fi-die-three:before { content: "\f13e"; }
+.fi-die-two:before { content: "\f13f"; }
+.fi-dislike:before { content: "\f140"; }
+.fi-dollar-bill:before { content: "\f141"; }
+.fi-dollar:before { content: "\f142"; }
+.fi-download:before { content: "\f143"; }
+.fi-eject:before { content: "\f144"; }
+.fi-elevator:before { content: "\f145"; }
+.fi-euro:before { content: "\f146"; }
+.fi-eye:before { content: "\f147"; }
+.fi-fast-forward:before { content: "\f148"; }
+.fi-female-symbol:before { content: "\f149"; }
+.fi-female:before { content: "\f14a"; }
+.fi-filter:before { content: "\f14b"; }
+.fi-first-aid:before { content: "\f14c"; }
+.fi-flag:before { content: "\f14d"; }
+.fi-folder-add:before { content: "\f14e"; }
+.fi-folder-lock:before { content: "\f14f"; }
+.fi-folder:before { content: "\f150"; }
+.fi-foot:before { content: "\f151"; }
+.fi-foundation:before { content: "\f152"; }
+.fi-graph-bar:before { content: "\f153"; }
+.fi-graph-horizontal:before { content: "\f154"; }
+.fi-graph-pie:before { content: "\f155"; }
+.fi-graph-trend:before { content: "\f156"; }
+.fi-guide-dog:before { content: "\f157"; }
+.fi-hearing-aid:before { content: "\f158"; }
+.fi-heart:before { content: "\f159"; }
+.fi-home:before { content: "\f15a"; }
+.fi-html5:before { content: "\f15b"; }
+.fi-indent-less:before { content: "\f15c"; }
+.fi-indent-more:before { content: "\f15d"; }
+.fi-info:before { content: "\f15e"; }
+.fi-italic:before { content: "\f15f"; }
+.fi-key:before { content: "\f160"; }
+.fi-laptop:before { content: "\f161"; }
+.fi-layout:before { content: "\f162"; }
+.fi-lightbulb:before { content: "\f163"; }
+.fi-like:before { content: "\f164"; }
+.fi-link:before { content: "\f165"; }
+.fi-list-bullet:before { content: "\f166"; }
+.fi-list-number:before { content: "\f167"; }
+.fi-list-thumbnails:before { content: "\f168"; }
+.fi-list:before { content: "\f169"; }
+.fi-lock:before { content: "\f16a"; }
+.fi-loop:before { content: "\f16b"; }
+.fi-magnifying-glass:before { content: "\f16c"; }
+.fi-mail:before { content: "\f16d"; }
+.fi-male-female:before { content: "\f16e"; }
+.fi-male-symbol:before { content: "\f16f"; }
+.fi-male:before { content: "\f170"; }
+.fi-map:before { content: "\f171"; }
+.fi-marker:before { content: "\f172"; }
+.fi-megaphone:before { content: "\f173"; }
+.fi-microphone:before { content: "\f174"; }
+.fi-minus-circle:before { content: "\f175"; }
+.fi-minus:before { content: "\f176"; }
+.fi-mobile-signal:before { content: "\f177"; }
+.fi-mobile:before { content: "\f178"; }
+.fi-monitor:before { content: "\f179"; }
+.fi-mountains:before { content: "\f17a"; }
+.fi-music:before { content: "\f17b"; }
+.fi-next:before { content: "\f17c"; }
+.fi-no-dogs:before { content: "\f17d"; }
+.fi-no-smoking:before { content: "\f17e"; }
+.fi-page-add:before { content: "\f17f"; }
+.fi-page-copy:before { content: "\f180"; }
+.fi-page-csv:before { content: "\f181"; }
+.fi-page-delete:before { content: "\f182"; }
+.fi-page-doc:before { content: "\f183"; }
+.fi-page-edit:before { content: "\f184"; }
+.fi-page-export-csv:before { content: "\f185"; }
+.fi-page-export-doc:before { content: "\f186"; }
+.fi-page-export-pdf:before { content: "\f187"; }
+.fi-page-export:before { content: "\f188"; }
+.fi-page-filled:before { content: "\f189"; }
+.fi-page-multiple:before { content: "\f18a"; }
+.fi-page-pdf:before { content: "\f18b"; }
+.fi-page-remove:before { content: "\f18c"; }
+.fi-page-search:before { content: "\f18d"; }
+.fi-page:before { content: "\f18e"; }
+.fi-paint-bucket:before { content: "\f18f"; }
+.fi-paperclip:before { content: "\f190"; }
+.fi-pause:before { content: "\f191"; }
+.fi-paw:before { content: "\f192"; }
+.fi-paypal:before { content: "\f193"; }
+.fi-pencil:before { content: "\f194"; }
+.fi-photo:before { content: "\f195"; }
+.fi-play-circle:before { content: "\f196"; }
+.fi-play-video:before { content: "\f197"; }
+.fi-play:before { content: "\f198"; }
+.fi-plus:before { content: "\f199"; }
+.fi-pound:before { content: "\f19a"; }
+.fi-power:before { content: "\f19b"; }
+.fi-previous:before { content: "\f19c"; }
+.fi-price-tag:before { content: "\f19d"; }
+.fi-pricetag-multiple:before { content: "\f19e"; }
+.fi-print:before { content: "\f19f"; }
+.fi-prohibited:before { content: "\f1a0"; }
+.fi-projection-screen:before { content: "\f1a1"; }
+.fi-puzzle:before { content: "\f1a2"; }
+.fi-quote:before { content: "\f1a3"; }
+.fi-record:before { content: "\f1a4"; }
+.fi-refresh:before { content: "\f1a5"; }
+.fi-results-demographics:before { content: "\f1a6"; }
+.fi-results:before { content: "\f1a7"; }
+.fi-rewind-ten:before { content: "\f1a8"; }
+.fi-rewind:before { content: "\f1a9"; }
+.fi-rss:before { content: "\f1aa"; }
+.fi-safety-cone:before { content: "\f1ab"; }
+.fi-save:before { content: "\f1ac"; }
+.fi-share:before { content: "\f1ad"; }
+.fi-sheriff-badge:before { content: "\f1ae"; }
+.fi-shield:before { content: "\f1af"; }
+.fi-shopping-bag:before { content: "\f1b0"; }
+.fi-shopping-cart:before { content: "\f1b1"; }
+.fi-shuffle:before { content: "\f1b2"; }
+.fi-skull:before { content: "\f1b3"; }
+.fi-social-500px:before { content: "\f1b4"; }
+.fi-social-adobe:before { content: "\f1b5"; }
+.fi-social-amazon:before { content: "\f1b6"; }
+.fi-social-android:before { content: "\f1b7"; }
+.fi-social-apple:before { content: "\f1b8"; }
+.fi-social-behance:before { content: "\f1b9"; }
+.fi-social-bing:before { content: "\f1ba"; }
+.fi-social-blogger:before { content: "\f1bb"; }
+.fi-social-delicious:before { content: "\f1bc"; }
+.fi-social-designer-news:before { content: "\f1bd"; }
+.fi-social-deviant-art:before { content: "\f1be"; }
+.fi-social-digg:before { content: "\f1bf"; }
+.fi-social-dribbble:before { content: "\f1c0"; }
+.fi-social-drive:before { content: "\f1c1"; }
+.fi-social-dropbox:before { content: "\f1c2"; }
+.fi-social-evernote:before { content: "\f1c3"; }
+.fi-social-facebook:before { content: "\f1c4"; }
+.fi-social-flickr:before { content: "\f1c5"; }
+.fi-social-forrst:before { content: "\f1c6"; }
+.fi-social-foursquare:before { content: "\f1c7"; }
+.fi-social-game-center:before { content: "\f1c8"; }
+.fi-social-github:before { content: "\f1c9"; }
+.fi-social-google-plus:before { content: "\f1ca"; }
+.fi-social-hacker-news:before { content: "\f1cb"; }
+.fi-social-hi5:before { content: "\f1cc"; }
+.fi-social-instagram:before { content: "\f1cd"; }
+.fi-social-joomla:before { content: "\f1ce"; }
+.fi-social-lastfm:before { content: "\f1cf"; }
+.fi-social-linkedin:before { content: "\f1d0"; }
+.fi-social-medium:before { content: "\f1d1"; }
+.fi-social-myspace:before { content: "\f1d2"; }
+.fi-social-orkut:before { content: "\f1d3"; }
+.fi-social-path:before { content: "\f1d4"; }
+.fi-social-picasa:before { content: "\f1d5"; }
+.fi-social-pinterest:before { content: "\f1d6"; }
+.fi-social-rdio:before { content: "\f1d7"; }
+.fi-social-reddit:before { content: "\f1d8"; }
+.fi-social-skillshare:before { content: "\f1d9"; }
+.fi-social-skype:before { content: "\f1da"; }
+.fi-social-smashing-mag:before { content: "\f1db"; }
+.fi-social-snapchat:before { content: "\f1dc"; }
+.fi-social-spotify:before { content: "\f1dd"; }
+.fi-social-squidoo:before { content: "\f1de"; }
+.fi-social-stack-overflow:before { content: "\f1df"; }
+.fi-social-steam:before { content: "\f1e0"; }
+.fi-social-stumbleupon:before { content: "\f1e1"; }
+.fi-social-treehouse:before { content: "\f1e2"; }
+.fi-social-tumblr:before { content: "\f1e3"; }
+.fi-social-twitter:before { content: "\f1e4"; }
+.fi-social-vimeo:before { content: "\f1e5"; }
+.fi-social-windows:before { content: "\f1e6"; }
+.fi-social-xbox:before { content: "\f1e7"; }
+.fi-social-yahoo:before { content: "\f1e8"; }
+.fi-social-yelp:before { content: "\f1e9"; }
+.fi-social-youtube:before { content: "\f1ea"; }
+.fi-social-zerply:before { content: "\f1eb"; }
+.fi-social-zurb:before { content: "\f1ec"; }
+.fi-sound:before { content: "\f1ed"; }
+.fi-star:before { content: "\f1ee"; }
+.fi-stop:before { content: "\f1ef"; }
+.fi-strikethrough:before { content: "\f1f0"; }
+.fi-subscript:before { content: "\f1f1"; }
+.fi-superscript:before { content: "\f1f2"; }
+.fi-tablet-landscape:before { content: "\f1f3"; }
+.fi-tablet-portrait:before { content: "\f1f4"; }
+.fi-target-two:before { content: "\f1f5"; }
+.fi-target:before { content: "\f1f6"; }
+.fi-telephone-accessible:before { content: "\f1f7"; }
+.fi-telephone:before { content: "\f1f8"; }
+.fi-text-color:before { content: "\f1f9"; }
+.fi-thumbnails:before { content: "\f1fa"; }
+.fi-ticket:before { content: "\f1fb"; }
+.fi-torso-business:before { content: "\f1fc"; }
+.fi-torso-female:before { content: "\f1fd"; }
+.fi-torso:before { content: "\f1fe"; }
+.fi-torsos-all-female:before { content: "\f1ff"; }
+.fi-torsos-all:before { content: "\f200"; }
+.fi-torsos-female-male:before { content: "\f201"; }
+.fi-torsos-male-female:before { content: "\f202"; }
+.fi-torsos:before { content: "\f203"; }
+.fi-trash:before { content: "\f204"; }
+.fi-trees:before { content: "\f205"; }
+.fi-trophy:before { content: "\f206"; }
+.fi-underline:before { content: "\f207"; }
+.fi-universal-access:before { content: "\f208"; }
+.fi-unlink:before { content: "\f209"; }
+.fi-unlock:before { content: "\f20a"; }
+.fi-upload-cloud:before { content: "\f20b"; }
+.fi-upload:before { content: "\f20c"; }
+.fi-usb:before { content: "\f20d"; }
+.fi-video:before { content: "\f20e"; }
+.fi-volume-none:before { content: "\f20f"; }
+.fi-volume-strike:before { content: "\f210"; }
+.fi-volume:before { content: "\f211"; }
+.fi-web:before { content: "\f212"; }
+.fi-wheelchair:before { content: "\f213"; }
+.fi-widget:before { content: "\f214"; }
+.fi-wrench:before { content: "\f215"; }
+.fi-x-circle:before { content: "\f216"; }
+.fi-x:before { content: "\f217"; }
+.fi-yen:before { content: "\f218"; }
+.fi-zoom-in:before { content: "\f219"; }
+.fi-zoom-out:before { content: "\f21a"; }
diff --git a/afm-client/app/Frontend/styles/foundation/_foundation-settings.scss b/afm-client/app/Frontend/styles/foundation/_foundation-settings.scss
new file mode 100644 (file)
index 0000000..d4c6415
--- /dev/null
@@ -0,0 +1,605 @@
+//  FOUNDATION FOR APPS SETTINGS
+//  ----------------------------
+//
+//  Table of Contents:
+//
+//  1.  CSS Exports
+//  2.  Global Styles
+//  3.  Breakpoints
+//  4.  Typography
+//  5.  Grid
+//  6.  Button
+//  7.  Accordion
+//  8.  Action Sheet
+//  9.  Block List
+//  10. Button Group
+//  11. Card
+//  12. Extras
+//  13. Forms
+//  14. Iconic
+//  15. Label
+//  16. Menu Bar
+//  17. Modal
+//  18. Motion UI
+//  19. Notification
+//  20. Off-canvas
+//  21. Panel
+//  22. Popup
+//  23. Switch
+//  24. Tabs
+//  25. Title Bar
+
+@import "helpers/functions";
+
+// 1. CSS Exports
+// - - - - - - - - - - - - - - -
+
+// Change any value in this map from "true" to "false" to disable that component's CSS class output. You'll still be able to use the component's mixins, but none of our pre-written classes will be in your CSS.
+
+ $include-css: (
+   accordion: true,
+   action-sheet: true,
+   block-list: true,
+   button: true,
+   button-group: true,
+   card: true,
+   coloring: true,
+   extras: true,
+   forms: true,
+   grid: true,
+   iconic: true,
+   label: true,
+   badge: true,
+   list: true,
+   menu-bar: true,
+   modal: true,
+   motion: true,
+   notification: true,
+   off-canvas: true,
+   panel: true,
+   popup: true,
+   switch: true,
+   tabs: true,
+   title-bar: true,
+   typography: true,
+   utilities: true,
+ ); 
+
+// 2. Global Styles
+// - - - - - - - - - - - - - - -
+
+// This sets 1rem to be 16px
+// $rem-base: 16px;
+
+// The default font-size is set to 100% of the browser style sheet (usually 16px)
+// for compatibility with browser-based text zoom or user-set defaults.
+
+// Since the typical default browser font-size is 16px, that makes the calculation for grid size.
+// If you want your base font-size to be different and not have it affect the grid breakpoints,
+// set $rem-base to $base-font-size and make sure $base-font-size is a px value.
+// $base-font-size: 100%;
+
+// $base-line-height is 24px while $base-font-size is 16px
+// $base-line-height: 1.5;
+
+// Text selector helpers
+// $headers: "h1,h2,h3,h4,h5,h6";
+
+// We use these to define default font weights
+// $font-weight-normal: normal;
+// $font-weight-bold: bold;
+
+// We use these to control various global styles
+// $body-background: #fff;
+// $body-font-color: #222;
+// $body-font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
+// $body-font-weight: $font-weight-normal;
+// $body-font-style: normal;
+// $body-antialiased: true;
+
+// Application Colors
+// $primary-color: #00558b;
+// $secondary-color: #f1f1f1;
+// $alert-color: #F04124;
+// $info-color: #A0D3E8;
+// $success-color: #43AC6A;
+// $warning-color: #F08A24;
+// $dark-color: #232323;
+// $gray: #dfdfdf;
+// $gray-dark: darken($gray, 8);
+// $gray-light: lighten($gray, 8);
+
+// We use these to make sure border radius matches unless we want it different.
+// $global-radius: 4px;
+// $global-rounded: 1000px;
+
+// We use this for default spacing
+// $global-padding: 1rem;
+// $global-spacing: rem-calc(15); 
+
+// 3. Breakpoints
+// - - - - - - - - - - - - - - -
+
+// These are our named breakpoints. You can use them in our breakpoint function like this: @include breakpoint(medium) { // Medium and larger styles }
+// $breakpoints: (
+//   small: rem-calc(0),
+//   medium: rem-calc(640),
+//   large: rem-calc(1200),
+//   xlarge: rem-calc(1440),
+//   xxlarge: rem-calc(1920),
+// );
+
+// All of the names in this list will be output as classes in your CSS, like small-12, medium-6, and so on.
+// $breakpoint-classes: (small medium large); 
+
+// 4. Typography
+// - - - - - - - - - - - - - - -
+
+// We use these to control header font styles
+// $header-font-family: $body-font-family;
+// $header-font-weight: $font-weight-normal;
+// $header-font-style: $font-weight-normal;
+// $header-font-color: #222;
+// $header-line-height: 1.4;
+// $header-top-margin: .2rem;
+// $header-bottom-margin: .5rem;
+// $header-text-rendering: optimizeLegibility;
+
+// We use these to control header font sizes
+// $h1-font-size: rem-calc(44);
+// $h2-font-size: rem-calc(37);
+// $h3-font-size: rem-calc(27);
+// $h4-font-size: rem-calc(23);
+// $h5-font-size: rem-calc(18);
+// $h6-font-size: 1rem;
+
+// We use these to control header size reduction on small screens
+// $h1-font-reduction: rem-calc(10);
+// $h2-font-reduction: rem-calc(10);
+// $h3-font-reduction: rem-calc(5);
+// $h4-font-reduction: rem-calc(5);
+// $h5-font-reduction: 0;
+// $h6-font-reduction: 0;
+
+// These control how subheaders are styled.
+// $subheader-line-height: 1.4;
+// $subheader-font-color: scale-color($header-font-color, $lightness: 35%);
+// $subheader-font-weight: $font-weight-normal;
+// $subheader-top-margin: .2rem;
+// $subheader-bottom-margin: .5rem;
+
+// A general <small> styling
+// $small-font-size: 60%;
+// $small-font-color: scale-color($header-font-color, $lightness: 35%);
+
+// We use these to style paragraphs
+// $paragraph-font-family: inherit;
+// $paragraph-font-weight: $font-weight-normal;
+// $paragraph-font-size: 1rem;
+// $paragraph-line-height: 1.6;
+// $paragraph-margin-bottom: rem-calc(20);
+// $paragraph-aside-font-size: rem-calc(14);
+// $paragraph-aside-line-height: 1.35;
+// $paragraph-aside-font-style: italic;
+// $paragraph-text-rendering: optimizeLegibility;
+
+// We use these to style <code> tags
+// $code-color: grayscale($primary-color);
+// $code-font-family: Consolas, 'Liberation Mono', Courier, monospace;
+// $code-font-weight: $font-weight-normal;
+// $code-background-color: scale-color($secondary-color, $lightness: 70%);
+// $code-border-size: 1px;
+// $code-border-style: solid;
+// $code-border-color: scale-color($code-background-color, $lightness: -10%);
+// $code-padding: rem-calc(2) rem-calc(5) rem-calc(1);
+
+// We use these to style anchors
+// $anchor-text-decoration: none;
+// $anchor-text-decoration-hover: none;
+// $anchor-font-color: $primary-color;
+// $anchor-font-color-hover: scale-color($anchor-font-color, $lightness: -14%);
+
+// We use these to style the <hr> element
+// $hr-border-width: 1px;
+// $hr-border-style: solid;
+// $hr-border-color: #ddd;
+// $hr-margin: rem-calc(20);
+
+// We use these to style lists
+// $list-font-family: $paragraph-font-family;
+// $list-font-size: $paragraph-font-size;
+// $list-line-height: $paragraph-line-height;
+// $list-margin-bottom: $paragraph-margin-bottom;
+// $list-style-position: outside;
+// $list-side-margin: 1.1rem;
+// $list-ordered-side-margin: 1.4rem;
+// $list-side-margin-no-bullet: 0;
+// $list-nested-margin: rem-calc(20);
+// $definition-list-header-weight: $font-weight-bold;
+// $definition-list-header-margin-bottom: .3rem;
+// $definition-list-margin-bottom: rem-calc(12);
+
+// We use these to style blockquotes
+// $blockquote-font-color: scale-color($header-font-color, $lightness: 35%);
+// $blockquote-padding: rem-calc(9 20 0 19);
+// $blockquote-border: 1px solid #ddd;
+// $blockquote-cite-font-size: rem-calc(13);
+// $blockquote-cite-font-color: scale-color($header-font-color, $lightness: 23%);
+// $blockquote-cite-link-color: $blockquote-cite-font-color;
+
+// Acronym styles
+// $acronym-underline: 1px dotted #ddd; 
+
+// 5. Grid
+// - - - - - - - - - - - - - - -
+
+// $container-width: rem-calc(900);
+// $block-padding: $global-padding;
+// $total-columns: 12;
+// $block-grid-max-size: 6; 
+
+// 6. Button
+// - - - - - - - - - - - - - - -
+
+// $button-padding: 0.85em 1em;
+// $button-margin: 0 $global-padding $global-padding 0;
+// $button-style: solid;
+// $button-background: $primary-color;
+// $button-color: auto;
+// $button-radius: 0;
+// $button-sizes: (
+//   tiny: 0.7,
+//   small: 0.8,
+//   medium: 1,
+//   large: 1.3,
+// );
+// $button-font-size: 0.9rem;
+// $button-opacity-disabled: 0.5;
+// $button-tag-selector: false; 
+
+// 7. Accordion
+// - - - - - - - - - - - - - - -
+
+// $accordion-border: 1px solid $gray-dark;
+
+// $accordion-title-background: $gray-light;
+// $accordion-title-background-hover: smartscale($accordion-title-background, 5%);
+// $accordion-title-background-active: smartscale($accordion-title-background, 3%);
+// $accordion-title-color: isitlight($accordion-title-background);
+// $accordion-title-color-active: isitlight($accordion-title-background);
+
+// $accordion-title-padding: $global-padding;
+// $accordion-content-padding: $global-padding; 
+
+// 8. Action Sheet
+// - - - - - - - - - - - - - - -
+
+// $actionsheet-background: white;
+// $actionsheet-border-color: #ccc;
+// $actionsheet-animate: transform opacity;
+// $actionsheet-animation-speed: 0.25s;
+// $actionsheet-width: 300px;
+// $actionsheet-radius: 4px;
+// $actionsheet-shadow: 0 -3px 10px rgba(black, 0.25);
+// $actionsheet-padding: $global-padding;
+// $actionsheet-tail-size: 10px;
+
+// $actionsheet-popup-shadow: 0 0 10px rgba(black, 0.25);
+
+// $actionsheet-link-color: #000;
+// $actionsheet-link-background-hover: smartscale($actionsheet-background); 
+
+// 9. Block List
+// - - - - - - - - - - - - - - -
+
+// $blocklist-background: #fff;
+// $blocklist-fullbleed: true;
+// $blocklist-fontsize: 1rem;
+
+// $blocklist-item-padding: 0.8rem 1rem;
+// $blocklist-item-color: isitlight($blocklist-background, #000, #fff);
+// $blocklist-item-background-hover: smartscale($blocklist-background, 4.5%);
+// $blocklist-item-color-disabled: #999;
+// $blocklist-item-border: 1px solid smartscale($blocklist-background, 18.5%);
+
+// $blocklist-item-label-color: scale-color($blocklist-item-color, $lightness: 60%);
+// $blocklist-item-icon-size: 0.8;
+
+// $blocklist-header-fontsize: 0.8em;
+// $blocklist-header-color: smartscale($blocklist-item-color, 40%);
+// $blocklist-header-uppercase: true;
+
+// $blocklist-check-icons: true; 
+
+// 10. Button Group
+// - - - - - - - - - - - - - - -
+
+// $btngroup-background: $primary-color;
+// $btngroup-color: #fff;
+// $btngroup-radius: $button-radius; 
+
+// 11. Card
+// - - - - - - - - - - - - - - -
+
+// $card-background: #fff;
+// $card-color: isitlight($card-background);
+// $card-border: 1px solid smartscale($card-background, 7%);
+// $card-radius: $global-radius;
+// $card-shadow: 0 1px 2px rgba(#000, 0.2);
+// $card-padding: $global-padding;
+// $card-margin: 0.5rem;
+
+// $card-divider-background: smartscale($card-background, 7%); 
+
+// 12. Extras
+// - - - - - - - - - - - - - - -
+
+// $closebutton-position: (top right);
+// $closebutton-size: 2em;
+// $closebutton-lineheight: 0.5;
+// $closebutton-color: #999;
+// $closebutton-color-hover: #333;
+
+// $thumbnail-padding: 0.5rem;
+// $thumbnail-shadow: 0 3px 15px rgba(black, 0.25); 
+
+// 13. Forms
+// - - - - - - - - - - - - - - -
+
+// Basic form variables
+// $form-fontsize: 1rem;
+// $form-padding: 0.5rem;
+
+// Text fields
+// $input-color: #000;
+// $input-color-hover: $input-color;
+// $input-color-focus: $input-color;
+// $input-background: #fff;
+// $input-background-hover: $input-background;
+// $input-background-focus: $input-background;
+// $input-border: 1px solid #ccc;
+// $input-border-hover: 1px solid #bbb;
+// $input-border-focus: 1px solid #999;
+
+// Select menus
+// $select-color: #000;
+// $select-background: #fafafa;
+// $select-background-hover: smartscale($select-background, 4%);
+// $select-arrow: true;
+// $select-arrow-color: $select-color;
+
+// Labels
+// $form-label-fontsize: 0.9rem;
+// $form-label-margin: 0.5rem;
+// $form-label-color: #333;
+
+// Inline labels
+// $inlinelabel-color: #333;
+// $inlinelabel-background: #eee;
+// $inlinelabel-border: $input-border;
+
+// Range slider
+// $slider-background: #ddd;
+// $slider-height: 1rem;
+// $slider-radius: 0px;
+// $slider-thumb-height: 1.5rem;
+// $slider-thumb-color: $primary-color;
+// $slider-thumb-radius: 0px;
+
+// Progress and meter
+// $meter-height: 1.5rem;
+// $meter-background: #ccc;
+// $meter-fill: $primary-color;
+// $meter-fill-high: $success-color;
+// $meter-fill-medium: #e7cf00;
+// $meter-fill-low: $alert-color;
+// $meter-radius: 0; 
+
+// 14. Iconic
+// - - - - - - - - - - - - - - -
+
+// $iconic-primary-fill: $primary-color;
+// $iconic-primary-stroke: $primary-color;
+// $iconic-accent-fill: $iconic-primary-fill;
+// $iconic-accent-stroke: $iconic-accent-fill; 
+
+// 15. Label
+// - - - - - - - - - - - - - - -
+
+// $label-fontsize: 0.8rem;
+// $label-padding: ($global-padding / 3) ($global-padding / 2);
+// $label-radius: 0;
+// $label-background: $primary-color;
+// $label-color: isitlight($primary-color);
+
+// $badge-fontsize: 0.8em;
+// $badge-diameter: 1.5rem;
+// $badge-background: $primary-color;
+// $badge-color: #fff;
+
+// DEPRECATED: these variables will be removed in v1.1.
+// $badge-padding: .1em .61em;
+// $badge-radius: $global-rounded;
+// $badge-font-color: #fff; 
+
+// 16. Menu Bar
+// - - - - - - - - - - - - - - -
+
+// $menubar-fontsize: 1rem;
+// $menubar-background: #fff;
+// $menubar-background-hover: smartscale($menubar-background, 7%);
+// $menubar-background-active: $menubar-background-hover;
+// $menubar-color: isitlight($menubar-background);
+// $menubar-color-hover: $menubar-color;
+// $menubar-color-active: $menubar-color-hover;
+
+// $menubar-item-padding: $global-padding;
+// $menubar-icon-size: 25px;
+// $menubar-icon-spacing: $menubar-item-padding; 
+
+// 17. Modal
+// - - - - - - - - - - - - - - -
+
+// $modal-background: #fff;
+// $modal-border: 0;
+// $modal-radius: 0px;
+// $modal-shadow: none;
+// $modal-zindex: 1000;
+// $modal-sizes: (
+//   tiny: 300px,
+//   small: 500px,
+//   medium: 600px,
+//   large: 800px,
+// );
+
+// $modal-overlay-class: 'modal-overlay';
+// $modal-overlay-background: rgba(#333, 0.7); 
+
+// 18. Motion UI
+// - - - - - - - - - - - - - - -
+
+// Classes to use when triggering in/out animations
+// $motion-class: (
+//   in: "ng-enter",
+//   out: "ng-leave",
+// );
+// $motion-class-active: (
+//   in: "ng-enter-active",
+//   out: "ng-leave-active",
+// );
+// $motion-class-stagger: (
+//   in: "ng-enter-stagger",
+//   out: "ng-leave-stagger",
+// );
+
+// Set if movement-based transitions should also fade the element in and out
+// $motion-slide-and-fade: false;
+// $motion-hinge-and-fade: true;
+// $motion-scale-and-fade: true;
+// $motion-spin-and-fade: true;
+
+// Default speed for transitions and animations
+// $motion-duration-default: 500ms;
+// Slow and fast modifiders
+// $motion-duration-slow: 750ms;
+// $motion-duration-fast: 250ms;
+// $motion-stagger-duration-default: 150ms;
+// $motion-stagger-duration-short: 50ms;
+// $motion-stagger-duration-long: 300ms;
+
+// Default timing function for transitions and animations
+// $motion-timing-default: ease;
+// Built-in and custom easing functions
+// Every item in this map becomes a CSS class
+// $motion-timings: (
+//   linear: linear,
+//   ease: ease,
+//   easeIn: ease-in,
+//   easeOut: ease-out,
+//   easeInOut: ease-in-out,
+//   bounceIn: cubic-bezier(0.485, 0.155, 0.240, 1.245),
+//   bounceOut: cubic-bezier(0.485, 0.155, 0.515, 0.845),
+//   bounceInOut: cubic-bezier(0.760, -0.245, 0.240, 1.245),
+// );
+
+// Default delay for all transitions and animations
+// $motion-delay-default: 0;
+// Short and long delay modifiers
+// $motion-delay-short: 300ms;
+// $motion-delay-long: 700ms; 
+
+// 19. Notification
+// - - - - - - - - - - - - - - -
+
+// $notification-default-position: right top;
+// $notification-width: rem-calc(400);
+// $notification-offset: $global-padding;
+
+// $notification-background: $primary-color;
+// $notification-color: white;
+// $notification-padding: $global-padding;
+// $notification-radius: 4px;
+
+// $notification-icon-size: 60px;
+// $notification-icon-margin: $global-padding;
+// $notification-icon-align: top;
+
+// 20. Off-canvas
+// - - - - - - - - - - - - - - -
+
+// $offcanvas-size-horizontal: 250px;
+// $offcanvas-size-vertical: 250px;
+
+// $offcanvas-background: #fff;
+// $offcanvas-color: isitlight($offcanvas-background);
+// $offcanvas-padding: 0;
+// $offcanvas-shadow: 3px 0 10px rgba(black, 0.25);
+// $offcanvas-animation-speed: 0.25s;
+
+// $offcanvas-frame-selector: '.grid-frame'; 
+
+// 21. Panel
+// - - - - - - - - - - - - - - -
+
+// $panel-size-horizontal: 300px;
+// $panel-size-vertical: 300px;
+// $panel-padding: 0;
+
+// $panel-background: #fff;
+// $panel-shadow: 3px 0 10px rgba(black, 0.25);
+// $panel-animation-speed: 0.25s; 
+
+// 22. Popup
+// - - - - - - - - - - - - - - -
+
+// $popup-width: rem-calc(300);
+// $popup-background: #fff;
+// $popup-border: 0;
+// $popup-radius: 0;
+// $popup-shadow: 0 0 10px rgba(#000, 0.25); 
+
+// 23. Switch
+// - - - - - - - - - - - - - - -
+
+// $switch-width: rem-calc(50);
+// $switch-height: rem-calc(32);
+// $switch-background: #ccc;
+// $switch-background-active: $primary-color;
+// $switch-border: 0;
+// $switch-radius: 9999px;
+// $switch-animation-speed: 0.15s;
+
+// $switch-paddle-color: white;
+// $switch-paddle-offset: 4px; 
+
+// 24. Tabs
+// - - - - - - - - - - - - - - -
+
+// $tabstrip-background: transparent;
+
+// $tab-title-background: $gray-light;
+// $tab-title-background-hover: smartscale($tab-title-background, 5%);
+// $tab-title-background-active: smartscale($tab-title-background, 3%);
+// $tab-title-color: isitlight($tab-title-background);
+// $tab-title-color-active: $tab-title-color;
+
+// $tab-title-padding: $global-padding;
+// $tab-content-padding: $global-padding; 
+
+// 25. Title Bar
+// - - - - - - - - - - - - - - -
+
+// $titlebar-center-width: 50%;
+// $titlebar-side-width: (100% - $titlebar-center-width) / 2;
+// $titlebar-background: #eee;
+// $titlebar-color: #000;
+// $titlebar-border: 1px solid #ccc;
+// $titlebar-padding: $global-padding;
+// $titlebar-item-classes: (
+//   center: 'center',
+//   left: 'left',
+//   right: 'right',
+//   title: 'title',
+// ); 
+
diff --git a/afm-client/app/Frontend/styles/foundation/foundation-conf.scss b/afm-client/app/Frontend/styles/foundation/foundation-conf.scss
new file mode 100644 (file)
index 0000000..bdcfe1f
--- /dev/null
@@ -0,0 +1,19 @@
+@import "foundation-settings";
+@import "foundation-icons";
+@import "foundation";
+
+
+/*.sidebar {
+    // Panel on small screens
+    @extend %panel-base;
+    @include panel-position(left);
+    // Override styles to become a block on medium screens
+    @include breakpoint(medium) {
+    @include grid-panel-reset;
+    @include grid-block(4);
+    }
+    // Change size to 3 columns on large screens
+    @include breakpoint(large) {
+    @include grid-size(3);
+    }
+}*/
\ No newline at end of file
diff --git a/afm-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss b/afm-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss
new file mode 100644 (file)
index 0000000..16f7bb5
--- /dev/null
@@ -0,0 +1,27 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "app/ibz-mixins";
+
+// place here your submit buttons customization
+appli-button {
+    @include ibz-button(grey,1rem)
+    img {
+        height: 3rem;
+    }
+}
diff --git a/afm-client/app/Frontend/widgets/ActionButtons/AppliButton.js b/afm-client/app/Frontend/widgets/ActionButtons/AppliButton.js
new file mode 100644 (file)
index 0000000..387212e
--- /dev/null
@@ -0,0 +1,130 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ */
+
+(function () {
+    'use strict';
+
+    var tmplAppli = '<div  ng-click="clicked()">' +
+            '<img ng-src={{icon}}-ico.png>' +
+            '<span>{{label}}</span>' +
+            '</div>';
+    
+    var tmplModal = 
+            '<b class="close-button" ng-click="close()">×</b>' +
+            '<img ng-src="{{appicon}}-ico.png">' +
+            '<span class="modal-text">Application <b>{{appname}}</b></span>' +
+            '<ul class="vertical icon-left primary menu-bar">' +
+            '<li><a ng-click=action("start")><i class="fi-check"> Start</i></a></li>' +
+            '<li><a href="#"><i class="fi-x"> Stop</i></a></li>' +
+            '<li><a href="#"><i class="fi-info"> Info</i></a></li>' +
+            '</ul>' +
+            '';
+
+    angular.module('AppliButton', [])
+            .directive('appliButton', function (AppConfig, AppCall, ModalFactory, Notification, $timeout) {
+
+                function mymethods(scope, elem, attrs) {
+                    scope.clicked = function () {
+                        
+                        var closeModal = function() {
+                            console.log ("Modal Closing");
+                            scope.modal.deactivate();
+                            $timeout (function() {scope.modal.destroy();}, 1000);
+                        };
+                        
+                        var actionModal = function(action) {
+                            console.log ("Modal Action=%s", action);
+                            switch (action) {
+                                
+                                case "start":
+                                    AppCall.get ("afm-main", "start", {id: scope.appliID}, function(response) {
+                                        if (response.status !== 200) {
+                                            Notification.error ({message: "Fail to start application=" + scope.label +" ID="+ scope.appliID, delay: 5000});
+                                            elem.addClass ("fail");
+                                            elem.removeClass ("success");
+                                            scope.callback (scope.appliID, "/api/afm-main/start", response);
+                                            return;
+                                        }
+
+                                        // Check this is a valid response from Binder
+                                        if (response.data.request.jtype !== "AJB_reply" && response.data.request.api !== "start") {
+                                            Notification.error ({message: "Invalid Respond to /opa/afm-main/start response.data="+response.data, delay: 5000}); 
+                                            elem.addClass ("fail");
+                                            elem.removeClass ("success");
+                                            scope.callback (scope.appliID, "/api/afm-main/start", response);
+                                            return;
+                                        }
+                                        
+                                        // Application was stated
+                                        scope.callback (scope.appliID, "/api/afm-main/start", response);
+                                    });
+                                    break;
+                                    
+                                case "stop":
+                                    break;
+                                    
+                                default:
+                                    console.log ("ActionModal unknown action=[%s]", action);
+                                    break;
+                            }
+                            
+                            closeModal();
+                        };
+            
+                        // reference http://foundation.zurb.com/apps/docs/#!/angular-modules
+                        var config = {
+                            animationIn: 'slideInFromTop',
+                            contentScope: {
+                                action  : actionModal,
+                                close   : closeModal,
+                                appicon : scope.icon,
+                                appname : scope.label,
+                            }, template : tmplModal
+                        }; 
+                        // Popup Modal to render application data
+                        scope.modal = new ModalFactory(config);
+                        scope.modal.activate ();
+                    };
+
+                    // extract application information from AppID+Store
+                    if (attrs.handle && scope.store [attrs.handle].name) {
+                        scope.icon  = AppConfig.paths.icons + scope.store [attrs.handle].name.toLowerCase();
+                        scope.label = scope.store [attrs.handle].name;
+                        scope.appliID= attrs.handle;
+                    } else {
+                         scope.icon  = AppConfig.paths.icons + 'w3c-ico.png';
+                         scope.label = attrs.handle;
+                    }
+                                
+                    // add label as class
+                    elem.addClass (scope.label.toLowerCase());
+                    
+                    // note: clicked in imported and when template is clicked
+                    // it will call clicked method passed in param.
+                }
+                
+                return {
+                    restrict: 'E',
+                    template: tmplAppli,
+                    link: mymethods,
+                    scope: {callback: '=', store: '='}
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/ActionButtons/SubmitButton.js b/afm-client/app/Frontend/widgets/ActionButtons/SubmitButton.js
new file mode 100644 (file)
index 0000000..323cd46
--- /dev/null
@@ -0,0 +1,52 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ */
+
+(function () {
+    'use strict';
+
+    var tmpl = '<div  ng-click="clicked()">' +
+            '<i class="{{icon}}"></i>' +
+            '<span>{{label}}</span>' +
+            '</div>';
+
+    angular.module('SubmitButton', [])
+            .directive('submitButton', function () {
+
+                function mymethods(scope, elem, attrs) {
+
+                    // ajust icon or use default
+                    scope.icon = attrs.icon || 'fi-foot';
+                    scope.label = attrs.label || 'Next';
+                                
+                    // add label as class
+                    elem.addClass (scope.label.toLowerCase());
+                    
+                    // note: clicked in imported and when template is clicked
+                    // it will call clicked method passed in param.
+                }
+                
+                return {
+                    restrict: 'E',
+                    template: tmpl,
+                    link: mymethods,
+                    scope: {clicked : '='}
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/FormInput/FormInput.scss b/afm-client/app/Frontend/widgets/FormInput/FormInput.scss
new file mode 100644 (file)
index 0000000..528ddfd
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+    Sample of style overload for a specific widget
+
+    Note: this SCSS is injected with main HTML page, it scope should be reduce
+    to a specific widget or it value will be propagated at a global level.
+*/
+
+@import "app/ibz-mixins";
+
+.upload-file {
+    @include ibz-button(grey,1rem)
+    float: right;
+    height  : 3rem;
+    margin: 0.5rem;
+
+    i { font-size: 2rem}
+    
+    .ibz-range-slider {
+        height: 10% !important;
+        border-radius: 5px;
+        background-color: lightgrey !important;
+    
+        .range-slider-handle {
+            width: 10% !important;
+            height: 100% !important;
+            margin-top: .2rem;
+            background-color: purple !important;
+        }
+
+        .range-slider-active-segment {
+            height: 80% !important;
+            background-color: lightgreen;
+        }
+    }
+
+}
+
+input-text {
+  
+    alert {@include ibz-input-alert(darkblue, rgba(200, 200, 200, 0.6))};
+
+    input {
+        margin-bottom: .5rem !important;
+    }
+
+    label {
+        margin-top: 1rem !important;
+    }
+    
+    .required {
+        color: blue;
+        float: right;
+        color: lightskyblue;
+    }
+    .required.valid {
+        color: green;
+    }
+    
+    .required.invalid {
+        color: plum;
+    }  
+
+    .status-untouch {
+        border-color: rgba(200, 200, 200, 0.6) !important;
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(200, 200, 200, 0.6) !important;
+        color: #696969 !important;
+    }
+
+    input:focus {
+        border-color: rgba(82,168,236,0.8) ;
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(82,168,236,0.8) !important;
+        transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s;
+        color: darkslateblue !important;
+        @extend shadow-transition;
+    }
+
+    .status-valid {
+        border-color: rgba(154, 205, 50, 0.6)!important;
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(154, 205, 50, 0.6)!important;
+        @extend shadow-transition;
+    }
+
+    .status-invalid {
+        border-color: rgba(154, 17, 69, 0.6);
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(154, 17, 69, 0.6)!important;
+        color: rgb(154, 17, 69);
+        @extend shadow-transition;
+    }
+
+}
diff --git a/afm-client/app/Frontend/widgets/FormInput/InputPassword.js b/afm-client/app/Frontend/widgets/FormInput/InputPassword.js
new file mode 100644 (file)
index 0000000..157009c
--- /dev/null
@@ -0,0 +1,79 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ */
+
+(function() {
+'use strict';
+
+var tmpl = '<input-text  class="password" tip="{{tip1}}"  placeholder="{{place1}}"' +
+           'label="{{label1}}" callback="valid1" name="{{name}}-1" value="pass1" required minlen="{{minlen}}" type="password" >' +
+           '</input-text>' + 
+           '<input-text  class="password" tip="tip2"  placeholder="{{place2}}"' +
+           'label="{{label2}}" callback="valid2" name="{{name}}-2" value="pass2" required minlen="{{minlen}}" type="password" > '+
+           '</input-text>';
+
+angular.module('InputPassword',[])
+
+.directive('inputPassword', function() {
+    function mymethods(scope, elem, attrs) {
+    
+    scope.valid1 = function (name, value) {
+        console.log ("Clicked InputPassword1 name=%s value=%s", name, value);        
+        scope.firstpwd = value;
+    };
+    
+    scope.valid2 = function (name, value, done) {        
+        console.log ("Clicked InputPassword2 name=%s value=%s", name, value);        
+        
+        // if both passwd equal then call form CB
+        if (scope.firstpwd !== value) {
+          done({valid: false, status: 'invalid', errmsg: "both password should match"});  
+        } else {  
+          scope.callback (attrs.name, value);
+        }
+                  
+     };
+     
+     // this method can be called from controller to update widget status
+     scope.done=function (data) {
+       console.log ("Text-Input Callback ID="+ attrs.name + " data=", data);
+       for (var i in data) scope[i] = data[i];         
+     };
+     
+     // Export some attributes within directive scope for template
+     scope.name   = attrs.name;
+     scope.label1 = attrs.label || 'Password';
+     scope.label2 = attrs.label || 'Password Verification';
+     scope.place1 = attrs.placeholder1 || 'User Password';
+     scope.tip1   = attrs.tip || 'Choose a Password';
+     scope.place2 = attrs.placeholder1 || 'Password Verification';
+     scope.tip2   = attrs.tip    || 'Confirme your Password';
+     scope.minlen = attrs.minlen || 10;
+     
+     if ("required" in attrs) scope.required = 'required';
+         
+    }
+    
+    return {
+        restrict: 'E',
+        template: tmpl,
+        link: mymethods,
+        scope: {
+            callback : '=',
+        }
+    };
+});
+
+console.log ("InputPassword Loaded");
+})();
diff --git a/afm-client/app/Frontend/widgets/FormInput/InputText.js b/afm-client/app/Frontend/widgets/FormInput/InputText.js
new file mode 100644 (file)
index 0000000..2653175
--- /dev/null
@@ -0,0 +1,179 @@
+
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details..
+ */
+
+
+
+(function() {
+'use strict';
+
+var tmpl = '<tip-modal tip="tip"></tip-modal>' +
+           '<label for="{{name}}-intext">{{label}} <i ng-show="required" ng-click="ToBeDefined" ' +
+           'class="required {{status}} fi-checkbox" title="Free Value But Mandatory Argument" alt="?"> &nbsp; </i></label>'+          
+           '<input '+
+           ' type="{{type}}" id="{{name}}-intext" placeholder="{{placeholder}}"  class="status-{{status}}"'+
+           ' ng-model="value" ng-blur="validate()" ng-focus="selected()" '+
+           ' ng-model-options="{ updateOn: \'default blur\', debounce: {default: 500, blur: 0} }"' +
+           '><alert data-ng-show="!valid&&errmsg">{{errmsg}}</alert>';
+
+var emailpatern = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
+
+angular.module('InputText',['JQueryEmu'])
+
+.directive('inputText', function(JQemu) {
+    function mymethods(scope, elem, attrs) {
+    
+    // default value at 1st rendering
+    scope.error  = false;
+    scope.valid  = false;
+    scope.status = 'untouch';
+   
+    scope.input = elem.find ("input");
+    scope.required = 0;
+    
+    // requirer is use to increment requested counter
+    if ("required" in attrs) {
+        scope.required = 1;
+        elem.addClass ("required");
+    }
+       
+     // user enter input reset error status
+     scope.selected = function () {
+        scope.error=false; 
+        scope.errmsg=false; 
+        scope.status = 'touch';
+     };   
+            
+     scope.validate = function () {
+         
+         // get value from input field bypassing Angular ng-model
+         console.log ("Clicked InputText name=%s value=%s valid=%s", scope.name, scope.value, scope.valid);        
+
+         // form is not untouched anymore
+         scope.parent.removeClass ("ng-pristine");
+
+         // if value not null clean up string
+         if (scope.value) {
+             scope.error=false; 
+            // remove leading and trailling space
+            scope.value = scope.value.trim();
+         
+            // remove any space is not allowed
+            if ('nospace' in attrs) {
+               scope.value=scope.value.replace(/\s/g, '');    
+            }
+         
+            if ('lowercase' in attrs) {
+               scope.value = scope.value.toLowerCase();
+            }
+         
+            // check minimum lenght
+            if ("minlen" in attrs) {
+              if (scope.value.length < attrs.minlen) {
+                 scope.status='invalid';
+                 scope.errmsg=scope.name + ': Mininum Lengh= ' + attrs.minlen + ' Characters';
+                 scope.error=true;
+              }
+            }
+            
+            if ('email' in attrs) {
+            if (!emailpatern.test (scope.value)) {
+                scope.status='invalid';
+                scope.errmsg='invalid email address';
+                scope.error=true;
+            }
+         }
+         
+        } else {
+            if (scope.required) {
+                 scope.status='invalid';
+                 scope.errmsg=scope.name + ': Required Attribute';
+                 scope.error=true; 
+            }
+        }
+                           
+         // If local control fail let's refuse input
+         if (scope.error) {
+             if (scope.required && scope.valid) {
+                 scope.valid = false;
+                 if (scope.l4acounter.validated > 0) scope.l4acounter.validated --;
+             } 
+             // use call to update form scope on form completeness
+             scope.callback (attrs.name, null, scope.done);
+         } else { 
+             // localcheck is OK backup may nevertheless change status to false
+            if (scope.required  && !scope.valid) scope.l4acounter.validated ++;
+            scope.status='valid';
+            scope.valid=true;
+            scope.callback (attrs.name, scope.value, scope.done);
+         }
+          
+     };
+     
+     // this method can be called from controller to update widget status
+     scope.done=function (data) {
+       console.log ("Text-Input Callback ID="+ attrs.name + " data=", data);
+       for (var i in data) scope[i] = data[i];         
+     };
+     
+     // Export some attributes within directive scope for template
+     scope.label       = attrs.label;
+     scope.name        = attrs.name;
+     scope.placeholder = attrs.placeholder;
+     scope.type        = attrs.type || "text";
+     scope.tip         = attrs.tip;
+
+     // search for form within parent elemnts
+     scope.parent = JQemu.parent (elem, "FORM");
+
+     // email enforce lowercase and nospace   
+     if ("email" in attrs) {
+        attrs.lowercase=true; 
+        attrs.nospace=true; 
+        attrs.minlen=6; 
+     }
+
+     if (scope.required) {
+         scope.l4acounter = scope.parent.data ("l4acounter");
+         if (!scope.l4acounter) { 
+            scope.l4acounter =  {required:1, validated:0};
+            console.log("Field "+scope.name+" is required (1st)");
+            scope.parent.data ("l4acounter", scope.l4acounter); 
+         } else {
+             console.log("Field "+scope.name+" is required");
+             scope.l4acounter.required ++;
+         }
+     }
+         
+     // refresh validation each time controler update value
+     scope.$watch ('value', function(){
+         if(scope.value) scope.validate(); }
+     );
+    
+    }
+    
+    return {
+        restrict: 'E',
+        template: tmpl,
+        link: mymethods,
+        scope: {
+            callback : '=',
+            value: '='
+        }
+    };
+});
+
+console.log ("InputText Loaded");
+})();
diff --git a/afm-client/app/Frontend/widgets/FormInput/UploadAppli.js b/afm-client/app/Frontend/widgets/FormInput/UploadAppli.js
new file mode 100644 (file)
index 0000000..d18f620
--- /dev/null
@@ -0,0 +1,230 @@
+
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details..
+ * 
+ * Reference:
+ *   https://developer.mozilla.org/en/docs/Web/API/FileReader 
+ *   https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications#Using_hidden_file_input_elements_using_the_click%28%29_method
+ *   https://uncorkedstudios.com/blog/multipartformdata-file-upload-with-angularjs
+ *   https://www.terlici.com/2015/05/16/uploading-files-locally.html
+ *   https://github.com/nervgh/angular-file-upload/blob/master/src/services/FileUploader.js
+ *   https://stuk.github.io/jszip/documentation/howto/read_zip.html
+ *   http://onehungrymind.com/zip-parsing-jszip-angular/
+ *   http://stackoverflow.com/questions/15341912/how-to-go-from-blob-to-arraybuffer
+ *   
+ *   Bugs: zip file sent even when flag as invalid 
+ */
+
+
+(function() {
+'use strict';
+
+var tmplAppli = '<input type="file" name="{{name}}-input" onchange="angular.element(this).scope().UpLoadFile(this.files)" accept="{{mimetype}}" style="display:none">'+
+            '<div class="upload-file" ng-click="imgClicked()">' +
+            '<i class="{{icon}}"></i> <span>{{label}}</span>' +
+            '<range-slider ng-show="!noslider" id="{{name}}-slider" automatic=true inithook="SliderInitCB"></range-slider>' +
+            '</div>';
+    
+var tmplModal = '<span class="modal-text">Upload Application <b>{{appname}}</b> ?</span>' +
+            '<div>'+
+            '<img ng-src="{{appicon}}">' +
+            '<submit-button icon="fi-x" label="Cancel" clicked="refused"></submit-button>'+
+            '<submit-button icon="fi-like" label="Install" clicked="accepted"></submit-button> ' +
+            '</div>';
+    
+
+// Service Create xform insert files in and Post it to url
+function LoadFileSvc (scope, files, fileCB) {
+    var xmlReq = new XMLHttpRequest();
+    var xform  = new FormData();
+    
+    // Update slider during Upload
+    xmlReq.upload.onprogress = function (event) {
+        var progress = Math.round(event.lengthComputable ? event.loaded * 100 / event.total : 0);
+        if (scope.slider) scope.slider.setValue (progress);
+    };
+
+    // Upload is finish let's notify controler callback
+    xmlReq.onload = function () {
+        scope.divElem.addClass ("success");
+        scope.divElem.removeClass ("error");
+        var response ={
+            status : xmlReq.status,
+            headers: xmlReq.getAllResponseHeaders() 
+        };
+        scope.callback (response);
+    };
+
+    xmlReq.onerror = function () {
+        scope.divElem.addClass ("error");
+        scope.divElem.removeClass ("success");
+    };
+
+    xmlReq.onabort = function () {
+        scope.divElem.addClass ("error");
+        scope.divElem.removeClass ("success");
+        var response ={
+            status : xmlReq.status,
+            headers: xmlReq.getAllResponseHeaders() 
+        };
+        scope.callback (response);
+    };
+    
+    this.postfile = function(posturl) { 
+        // everything looks OK let's Post it
+        xmlReq.open("POST", posturl , true);
+        xmlReq.send(xform);
+    };
+
+    for (var i = 0; i < files.length; i++) {
+        this.file = files[i];
+        console.log ("filetype=%s",this.file.type );
+        // Unknow Type !!! if (!this.file.type.match(scope.mimetype)) continue;
+
+        console.log ("Selected file=" + this.file.name + " size="+ this.file.size/1024 + " Type="+ this.file.type);
+
+        // File to upload is too big
+        if (this.file.size > scope.maxsize*1024) {
+            scope.thumbnail = scope.istoobig; // warning if image path is wrong nothing happen
+            scope.$apply('thumbnail'); // we short-circuit Angular resync Image
+            return;
+        }
+
+        // This is not an uploadable file
+        if(isNaN(this.file.size)) {
+            scope.thumbnail = scope.isnotvalid; 
+            scope.$apply('thumbnail');
+            return;
+        }
+
+        this.basename= this.file.name.split('/').reverse()[0];
+        //scope.imgElem[0].file = this.file;
+
+        // If File is an image let display it now
+        if (fileCB) {
+            var reader = new FileReader();
+            reader.readAsArrayBuffer(this.file);
+            reader.onload = fileCB;
+        } 
+        // if everything is OK let's add file to xform
+        xform.append(scope.name, this.file, this.file.name);
+    }
+
+}
+
+angular.module('UploadFiles',['AppConfig', 'ModalNotification', 'RangeSlider'])
+
+.directive('uploadAppli', function(AppConfig,  JQemu, Notification, ModalFactory, $timeout) {
+    function mymethods(scope, elem, attrs) {
+        
+        // get widget image handle from template
+        scope.inputElem  = elem.find('input');
+        scope.divElem    = elem.find('div');
+        
+        // Image was ckick let's simulate an input (file) click
+        scope.imgClicked = function () {
+            scope.inputElem[0].click(); // Warning Angular TriggerEvent does not work!!!
+        };
+        
+        // Slider control handle registration after creation
+        scope.SliderInitCB=function (slider) {
+           scope.slider= slider; 
+        };
+        
+        // Upload is delegated to a shared function
+        scope.UpLoadFile=function (files) {
+            var handle; 
+            var appicon;
+            
+            var accepted = function() {
+                console.log ("Modal Accepted");
+                // This Looks OK let's Post Xform/File
+                handle.postfile(attrs.posturl + "?token=" + AppConfig.session.token);
+
+                scope.modal.deactivate();
+                $timeout (function() {scope.modal.destroy();}, 1000);
+            };
+            
+            var refused = function() {
+                console.log ("Modal Refused");
+                scope.modal.deactivate();
+                $timeout (function() {scope.modal.destroy();}, 1000);
+            };
+                       
+            var readerCB = function (upload) {
+
+                var zipapp = new JSZip (upload.target.result);
+                var thumbnail = zipapp.file("icon_128.png");
+                
+                // Check is we have a thumbnail within loaded Zipfile
+                if (!thumbnail) {
+                    console.log ("This is not a valid Application Framework APP");
+                    scope.thumbnail=AppConfig.paths[scope.category] + 'isnotvalid.png';
+                    scope.$apply('thumbnail'); // we short-circuit Angular resync Image
+                } else {
+                    //scope.imgElem[0].src = window.URL.createObjectURL(new Blob([thumbnail.asArrayBuffer()], {type: "image"}));
+                    appicon = window.URL.createObjectURL(new Blob([thumbnail.asArrayBuffer()], {type: "image"}));
+                    
+                    // reference http://foundation.zurb.com/apps/docs/#!/angular-modules
+                    var config = {
+                        animationIn: 'slideInFromTop',
+                        contentScope: {
+                            accepted: accepted,
+                            refused:  refused,
+                            appicon:  appicon,
+                            appname:  handle.basename
+                        }, template:  tmplModal
+                    }; 
+                    // Popup Modal to render application data
+                    scope.modal = new ModalFactory(config);
+                    scope.modal.activate ();
+                }
+            };
+            
+            // Load file within browser and if OK call readerCB
+            handle = new LoadFileSvc (scope, files, readerCB);
+        };
+
+        // Initiallize default values from attributes values
+        scope.name= attrs.name || 'appli';
+        scope.category= attrs.category  || 'appli';
+        scope.mimetype= (attrs.accept || '.wgt');
+        scope.maxsize = attrs.maxsize || 100000; // default max size 100MB
+        scope.regexp  = new RegExp (attrs.accept+ '.*','i');
+        scope.icon    = attrs.icon || 'fi-upload';
+        scope.label   = attrs.label || 'Upload';
+        
+        if (attrs.thumbnail) scope.isnotvalid= AppConfig.paths[scope.category] +  attrs.isnotvalid;
+        else  scope.isnotvalid=AppConfig.paths[scope.category] + 'isnotvalid.png';
+
+        if (attrs.istoobig) scope.istoobig= AppConfig.paths[scope.category] +  attrs.istoobig;
+        else  scope.istoobig=AppConfig.paths[scope.category] + 'istoobig.png';
+        scope.noslider = attrs.noslider || false;
+
+        if (!attrs.posturl) throw new TypeError('file-upload %s posturl=/api/xxxx/xxxx required', scope.attrs);            
+    }
+    return {
+        restrict: 'E',
+        template: tmplAppli,
+        link: mymethods,
+        scope: {
+            callback : '='
+        }
+    };
+    
+});
+
+console.log ("UploadFile Loaded");
+})();
diff --git a/afm-client/app/Frontend/widgets/Navigation/LinkButton.js b/afm-client/app/Frontend/widgets/Navigation/LinkButton.js
new file mode 100644 (file)
index 0000000..3e83425
--- /dev/null
@@ -0,0 +1,57 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ */
+
+(function () {
+    'use strict';
+
+    var tmpl = '<span title="Goto: {{href}}" ng-click="clicked()">' +
+            '<i class="{{icon}}"></i>' +
+            '<span>{{label}}</span>' +
+            '</span>';
+
+
+    angular.module('LinkButton', [])
+            .directive('linkButton', function ($location) {
+                
+                function mymethods(scope, elem, attrs) {
+
+                    scope.clicked = function () {
+                        
+                        if (!attrs.query) $location.path(attrs.href);
+                        else $location.path(attrs.href).search(attrs.query);
+                    };
+
+                    // ajust icon or use default
+                    scope.icon = attrs.icon   || 'fi-link';
+                    scope.label = attrs.label || 'Jump';
+                    scope.href  = attrs.href  || '/home';
+                    
+                    // add label as class
+                    elem.addClass (scope.label.toLowerCase());
+                }
+
+                return {
+                    restrict: 'E',
+                    template: tmpl,
+                    link: mymethods,
+                    scope: {}
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/Navigation/Navigation.scss b/afm-client/app/Frontend/widgets/Navigation/Navigation.scss
new file mode 100644 (file)
index 0000000..2babf24
--- /dev/null
@@ -0,0 +1,26 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "app/ibz-mixins";
+
+
+link-button {@include ibz-button(#3366ff,1rem)};
+
+//pale blue for secondaty link
+link-button.secondary {@include ibz-button(#99b3ff,1rem)};
+
diff --git a/afm-client/app/Frontend/widgets/Notifications/ModalNotification.js b/afm-client/app/Frontend/widgets/Notifications/ModalNotification.js
new file mode 100644 (file)
index 0000000..37ba047
--- /dev/null
@@ -0,0 +1,85 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ * 
+ * ref: https://developer.mozilla.org/en-US/docs/Web/Events/mouseover
+ * 
+ * usage: 
+ * 
+ * tipModal: listen event from elem.parent() to display tip-modal
+ *      <div class="xxxx">
+ *          <tip-modal tip=xxxx></tip-modal>
+ *          <input-text ....></input-text>
+ *      </div>
+ *      
+ * Note: use CSS.visibility to avoid display flickering at initial display.
+ */
+
+(function () {
+    'use strict';
+
+    var tmpl = '<div class="tip-modal-popup">' +
+            '<i class="{{icon}}"></i>' +
+            '<span>{{tip}}</span>' +
+            '</span></div>' ;
+
+    angular.module('ModalNotification', [])
+            .directive('tipModal', function ($timeout) {
+
+                function mymethods(scope, elem, attrs) {
+                    scope.parent = elem.parent();
+                    scope.modal    = elem.find("div");
+                    
+    
+                    // delay tip display to avoid blinking when moving mouse fast
+                    function display () {
+                        function action() {
+                             if (scope.show) scope.modal.css({opacity: 1, visibility:'visible'});  
+                        }
+                        scope.show = true;
+                        scope.timeout = $timeout(action, scope.delay);
+                    }
+                    
+                    function close () {
+                      scope.show = false;                     
+                      scope.modal.css({opacity: 0, visibility:'hidden'});
+                    }
+                    
+
+                    // ajust icon or use default
+                    scope.icon  = attrs.icon || 'fi-lightbulb';
+                    
+                    // Update Parent element to get mouse event
+                    scope.parent.addClass ('as-modal-tip');
+                    scope.parent.bind('click', close);
+                    scope.parent.bind('focus', display);
+                    scope.parent.bind('mouseover', display);
+                    scope.parent.bind('mouseleave', close);
+                    scope.parent.bind('blur', close);
+                    
+                    scope.delay = attrs.delay || 1000; // wait 1s before displaying tip
+                }
+
+                return {
+                    restrict: 'E',
+                    template: tmpl,
+                    link: mymethods,
+                    scope: {tip: "="} // tip may not be defined when widget is display
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/Notifications/Notifications.scss b/afm-client/app/Frontend/widgets/Notifications/Notifications.scss
new file mode 100644 (file)
index 0000000..fb740b7
--- /dev/null
@@ -0,0 +1,63 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Reference: http://www.greywyvern.com/?post=337
+ */
+
+@import "app/ibz-mixins";
+
+
+link-button {@include ibz-button(#3366ff,1rem)};
+
+// Modal should be relative and tip-modal-popup absolute
+tip-modal {
+    position:relative;
+}
+
+.tip-modal-popup {
+    //visibility: hidden;
+    width: 20rem;
+    position:absolute;
+    top:1em;
+    padding: 0.2em 0.6em;
+    border:1px solid #996633;
+    background-color:#e5ffff;
+    color:#000;
+    opacity:0;
+    transition:visibility .5s linear 1s,opacity 1s linear;
+    border-radius: 5px;
+    i {
+        margin: 0 .3rem 0 0;
+        display: inline;
+    }
+}
+
+token-refresh {
+    @include ibz-button(grey,1rem)
+    i {margin-left: .5rem;}
+    margin-right: 1rem;
+}
+
+token-refresh.online {
+    color: #0066cc;
+    i {color: lime;}    
+}
+
+token-refresh.offline {
+    color: #ff00ff;
+    i {color: red;}    
+}
diff --git a/afm-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js b/afm-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js
new file mode 100644 (file)
index 0000000..5c5b5ae
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ alsa-gateway -- provide a REST/HTTP interface to ALSA-Mixer
+
+ Copyright (C) 2015, Fulup Ar Foll
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with scope program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ References:
+
+ */
+
+(function () {
+    'use strict';
+
+    var template =
+          '<div class="afb-monitor" ng-click="getping()">' +
+         '<span class="afb-refresh-token"  >afb://{{hostname}}:{{httpdport}}</span>' +
+         '<i class="{{icon}}"></i>' +
+         '</div>';
+
+
+// scope module is load statically before any route is cativated
+angular.module('TokenRefresh', ['AppConfig', 'ModalNotification'])
+
+    .directive ('tokenRefresh', function($timeout, $http, $location, Notification, AppConfig) {
+
+    function mymethods(scope, elem, attrs) {
+        scope.status=undefined; // neither thu neither false
+        
+    
+        scope.online = function () {
+            elem.addClass    ("online");
+            elem.removeClass ("offline");
+        };
+
+        scope.offline = function(){
+            elem.addClass    ("offline");
+            elem.removeClass ("online");
+        };
+        
+        scope.onerror = function(data, errcode, headers) {
+            if (scope.status !== false)  {
+                Notification.warning ({message: "AppFramework Binder Lost", delay: 5000});
+                scope.offline();
+            }
+            scope.status = 0;
+        };
+        
+        scope.onsuccess = function(data, errcode, headers, config) {
+            if (scope.status !== true)  {
+                if (data.request.token) AppConfig.session.token = data.request.token;
+                if (data.request.uuid)  AppConfig.session.uuid  = data.request.uuid;
+                if (data.request.timeout)  AppConfig.session.timeout  = data.request.timeout;
+
+                Notification.success ({message: "AppFramework Binder Back to Live", delay: 3000});
+                scope.online();
+                if (scope.callback) scope.callback();
+            }
+            scope.status = 1;
+        };
+
+        // Check Binder status
+        scope.getping = function() {
+
+            var handler = $http.get(AppConfig.session.ping+'?token='+ AppConfig.session.token);
+            
+            // process success and error
+            handler.success(scope.onsuccess);
+            handler.error(scope.onerror);
+
+            // restart a new timer for next ping
+            $timeout (scope.getping, AppConfig.session.pingrate*1000);
+        };
+        
+        // Check Binder status
+        scope.refresh = function() {
+            var handler = $http.get(AppConfig.session.refresh+'?token='+ AppConfig.session.token);
+            
+            // process success and error
+            handler.success(scope.onsuccess);
+            handler.error(scope.onerror);
+            // restart a new timer for next refresh to 1/4 of timeout session
+            $timeout (scope.refresh, AppConfig.session.timeout *250);
+        };
+        
+        // Initial connection
+        scope.tkcreate = function() {
+            var handler = $http.get(AppConfig.session.create+'?token='+ AppConfig.session.initial);
+            
+            // process success and error
+            handler.success(scope.onsuccess);
+            handler.error(scope.onerror);
+        };
+        scope.icon      = attrs.icon   || "fi-lightbulb";
+        scope.hostname  = $location.host();
+        scope.httpdport = $location.port();
+        scope.autolog   = JSON.parse(attrs.autolog || false);
+        
+        if (scope.autolog) scope.tkcreate();
+
+        // Init ping and refresh process
+        $timeout (scope.getping, AppConfig.session.pingrate*1000);
+        $timeout (scope.refresh, AppConfig.session.timeout *250);
+    }
+
+    return {
+        template: template,
+        scope: {
+            callback : "="
+        },
+        restrict: 'E',
+        link: mymethods
+    };
+});
+
+})();
+console.log ("Token Refresh Loaded");
+
diff --git a/afm-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js b/afm-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js
new file mode 100644 (file)
index 0000000..77f0fce
--- /dev/null
@@ -0,0 +1,631 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ * 
+ * ref: https://developer.mozilla.org/en-US/docs/Web/Events/mouseover
+ * 
+ * usage: 
+Usage  <range-slider>
+---------------------
+   <range-slider
+      id="my-slider-name"                     // only use as an argument to callback
+      class="my-custom-class"                 // default class is ibz-range-slider
+      placeholder="Track Date Selection"      // place holder for date readonly input zone
+
+      <!-- Foundation classes -->
+      class="radius"                          // check Zurb foundation doc for further info.
+      class="ibz-handle-display"              // increase handle width to hold slider current value
+
+      <!-- Angular Scope Variables -->
+      callback="myCallBack"                    // $scope.myCallBack(sliderhandle) is called when ever slider handle blur
+      formatter="SliderFormatCB"               // $scope.myFormatter(value, sliderid) when exist is call when ever slider handle moves. Should return external form of slider value.
+      ng-model="xxxxxx"                        // xxx Must be defined, script will store a new RangerObject within provided ng-model variable.
+      start-at="ScopeVar"                      // Dynamic limitation when slider is constrains by an external componant [ex: check in/out]
+      stop-at="ScopeVar"                       // Idem but for end.
+
+      <!-- Angular Directive Attributes -->
+      not-less="integer"                       // Fixed starting value for slider [default 0]
+      not-more="integer"                       // Fixed end value for sliders [default 100]
+      by-step="+-integer"                      // If by-step is >0 then slider use it as step-value, when negative use it for decimal precision
+      display-target="handle"                  // display slider external formated value in the handle [requirer calss="ibz-handle-display"]
+      dual-handles='true'                      // add a second handle to slider for min/max range
+      initial='value|[start/stop]'             // slider initial value [dual-handles] may have initial values
+   /></range-slider>
+ */
+
+(function () {
+    'use strict';
+
+var RangeSlider = angular.module('RangeSlider',[]);
+
+function RangeSliderHandle (scope) {
+    var internals = [];
+    var externals = [];
+
+    this.getId = function() {
+        return scope.sliderid;
+    };
+
+    this.getCbHandle = function() {
+        return scope.cbhandle;
+    };
+
+    this.getView= function (handle) {
+        if (!handle) handle = 0;
+
+        // if value did not change return current external representation
+        if (scope.value[handle] === internals[handle]) return externals[handle];
+
+        // build external representation and save it for further requests
+        internals[handle] = scope.value[handle];
+        if (scope.formatter) externals[handle] = scope.formatter(scope.value[handle], scope.ctrlhandle);
+        else  externals[handle] = scope.value[handle];
+
+        return externals[handle];
+    };
+
+    this.updateClass = function (classe, status) {
+       scope.updateClass (classe, status);
+    };
+
+    this.forceRefresh = function (timer) {
+       scope.forceRefresh(timer);
+    };
+
+    this.getValue= function (handle) {
+        if (!handle) handle = 0;
+        return scope.value[handle];
+    };
+
+    this.getRelative= function (handle) {
+        if (!handle) handle = 0;
+        return scope.relative[handle];
+    };
+
+    this.setValue= function (value, handle) {
+        if (!handle) handle = 0;
+        scope.setValue (value, handle);
+    };
+
+    this.setDisable= function (flag) {
+        scope.setDisable(flag);
+    };
+}
+
+RangeSlider.directive('rangeSlider', function ($log, $document, $timeout) {
+
+    var template= '<div class="ibz-range-slider range-slider" title="{{title}}"data-slider>'+
+                  '<span class="range-slider-handle handle-min" ng-mousedown="handleCB($event,0)" ng-focus="focusCB(true)" ng-blur="focusCB(false)" role="slider" tabindex="0"></span>'+
+                  '<span class="handle-max" ng-mousedown="handleCB($event,1)" ng-focus="focusCB(true)" ng-blur="focusCB(false)" role="slider" tabindex="0"></span>'+
+                  '<span class="range-slider-active-segment"></span>'+
+                  '<span class="ibz-range-slider-start" ></span> '+
+                  '<span class="ibz-range-slider-stop"></span> '+
+                  '<input id={{sliderid}} type="hidden">'+
+                  '</div>';
+
+
+    function link (scope, element, attrs, model) {
+        // full initialisation of slider from a single object
+        scope.initWidget = function (initvalues) {
+
+            if (initvalues.byStep)  scope.byStep  = parseInt(initvalues.byStep);
+            if (initvalues.notMore) scope.notMore = parseInt(initvalues.notMore);
+            if (initvalues.notLess) scope.notLess = parseInt(initvalues.notLess);
+            if (initvalues.id)      scope.sliderid= initvalues.id;
+
+            // hugely but in some case DOM is not finish when we try to set values !!!
+            if (initvalues.value !== undefined)   {
+                scope.value = initvalues.value;
+                scope.forceRefresh (50); // wait 50ms for DOM to be ready
+            }
+        };
+
+        // this function recompute slide positioning
+        scope.forceRefresh = function (timer) {
+           var value = scope.value;
+           scope.value = [undefined,undefined];
+           $timeout (function() {
+               scope.setValue(value[0],0);
+               if (scope.dual)  scope.setValue(value[1],1);
+           }, timer);
+        };
+
+        // handler to change class from slider handle
+        scope.updateClass = function (classe, status) {
+
+            if (status) element.addClass (classe);
+            else  element.removeClass (classe);
+        };
+
+        scope.setDisable = function (disabled) {
+
+            if (disabled) {
+                element.addClass ("disable");
+                scope.handles[0].css ('visibility','hidden');
+                if (scope.dual) {
+                    scope.handles[1].css ('visibility','hidden');
+                }
+            } else {
+                element.removeClass ("disable");
+                scope.handles[0].css ('visibility','visible');
+                if (scope.dual) scope.handles[1].css ('visibility','visible');
+            }
+
+        };
+
+        scope.normalize = function (value) {
+            var result;
+            var range = scope.notMore - scope.notLess;
+            var point = value * range;
+
+            // if step is positive let's round step by step
+            if (scope.byStep >  0) {
+                var mod = (point - (point % scope.byStep)) / scope.byStep;
+                var rem = point % scope.byStep;
+
+                var round = (rem >= scope.byStep * 0.5 ? scope.byStep : 0);
+                result= (mod * scope.byStep + round) + scope.notLess;
+                //console.log ("range=%d value=%d point=%d mod=%d rem=%d round=%d result=%d", range, value, point, mod, rem, round, result)
+                return result;
+            }
+
+            // if step is negative return round to asked decimal
+            if (scope.byStep <  0) {
+                var power  =  Math.pow (10,(scope.byStep * -1));
+                result = scope.notLess + parseInt (point * power) / power;
+                return (result);
+            }
+
+            // if step is null return full value
+            return point;
+       };
+
+        // return current value
+        scope.getValue = function (offset, handle) {
+            if (scope.vertical) {
+                scope.relative[handle] = (offset - scope.bounds.handles[handle].getBoundingClientRect().height) / (scope.bounds.bar.getBoundingClientRect().height - scope.bounds.handles[handle].getBoundingClientRect().height);
+            } else {
+                scope.relative[handle] = offset /  (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
+            }
+
+            var newvalue = scope.normalize (scope.relative[handle]);
+
+
+            // if internal value change update or model
+            if (newvalue !== scope.value[handle]) {
+                if (newvalue < scope.startValue) newvalue=scope.startValue;
+                if (newvalue > scope.stopValue)  newvalue=scope.stopValue;
+
+
+                if (scope.formatter) {
+                    scope.viewValue = scope.formatter (newvalue, scope.ctrlhandle);
+                } else {
+                    scope.viewValue = newvalue;
+                }
+                if (scope.displays[handle]) {
+                    scope.displays[handle].html (scope.viewValue);
+                }
+
+                // update external representation of the model
+                scope.value[handle] = newvalue;
+                if (model) model.$setViewValue (scope.viewValue);
+                scope.$apply();
+                if (newvalue > scope.startValue && newvalue < scope.stopValue) scope.translate(offset, handle);
+            }
+        };
+
+
+        scope.setStart = function (value) {
+            var offset;
+            
+            if (value > scope.value[0]) {
+                if (!scope.dual) scope.setValue (value,0);
+                else scope.setValue (value,1);
+            }
+
+            if (scope.vertical) {
+                offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.start.css('height',offset + 'px');
+            } else {
+                offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.start.css('width',offset + 'px');
+            }
+
+            scope.startValue= value;
+        };
+
+        scope.setStop = function (value) {
+            var offset;
+            
+            if (value < scope.value[0]) {
+                if (!scope.dual) scope.setValue (value,0);
+                else scope.setValue (value,1);
+            }
+
+            if (scope.vertical) {
+                offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.start.css('height',offset + 'px');
+            } else {
+                offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.stop.css({'right': 0, 'width': (scope.bounds.bar.getBoundingClientRect().width  - offset) + 'px'});
+            }
+
+            scope.stopValue= value;
+        };
+
+        scope.translate = function (offset, handle) {
+            var start;
+            
+            if (scope.vertical) {
+                // take handle size in account to compute middle
+                var voffset = scope.bounds.bar.getBoundingClientRect().height - offset;
+
+                scope.handles[handle].css({
+                    '-webkit-transform': 'translateY(' + voffset + 'px)',
+                    '-moz-transform': 'translateY(' + voffset + 'px)',
+                    '-ms-transform': 'translateY(' + voffset + 'px)',
+                    '-o-transform': 'translateY(' + voffset + 'px)',
+                    'transform': 'translateY(' + voffset + 'px)'
+               });
+               if (!scope.dual) scope.slider.css('height', offset + 'px');
+               else if (scope.relative[1] && scope.relative[0]) {
+                   var height = (scope.relative[1] - scope.relative[0]) *  scope.bounds.bar.getBoundingClientRect().height;
+                   start  = (scope.relative[0] *  scope.bounds.bar.getBoundingClientRect().height);
+                   scope.slider.css ({'bottom': start+'px','height': height + 'px'});
+               }
+            } else {
+
+                scope.handles[handle].css({
+                    '-webkit-transform': 'translateX(' + offset + 'px)',
+                    '-moz-transform': 'translateX(' + offset + 'px)',
+                    '-ms-transform': 'translateX(' + offset + 'px)',
+                    '-o-transform': 'translateX(' + offset + 'px)',
+                    'transform': 'translateX(' + offset + 'px)'
+                });
+                if (!scope.dual) scope.slider.css('width',offset + 'px');
+                else if (scope.relative[1] && scope.relative[0]) {
+                    var width = (scope.relative[1] - scope.relative[0]) *  scope.bounds.bar.getBoundingClientRect().width;
+                    start = (scope.relative[0] *  scope.bounds.bar.getBoundingClientRect().width);
+                    scope.slider.css ({'left': start+'px','width': width + 'px'});
+                }
+            }
+        };
+
+        // position handle on the bar depending a given value
+        scope.setValue = function (value , handle) {
+            var offset;
+
+            // if value did not change ignore
+            if (value === scope.value[handle]) return;
+            if (value === undefined)   value=0;
+            if (value > scope.notMore) value=scope.notMore;
+            if (value < scope.notLess) value=scope.notLess;
+
+            if (scope.vertical) {
+                scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
+                if (handle === 0) offset = (scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height) + scope.bounds.handles[handle].getBoundingClientRect().height/2;
+                if (handle === 1) offset = scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height;
+
+            } else {
+                scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
+                offset = scope.relative[handle] *  (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
+            }
+
+            scope.translate (offset,handle);
+            scope.value[handle] = value;
+
+            if (scope.formatter) {
+                // when call through setValue we do not pass cbHandle
+                scope.viewValue = scope.formatter (value, undefined);
+            } else {
+                scope.viewValue = value;
+            }
+
+            if (model) model.$setViewValue( scope.viewValue);
+
+            if (scope.displays[handle]) {
+                scope.displays[handle].html (scope.viewValue);
+            }
+        };
+
+
+        // Minimal keystroke handling to close picker with ESC [scope.actif is current handle index]
+        scope.keydown=  function(e){
+
+            switch(e.keyCode){
+                case 39: // Right
+                case 38: // up
+                     if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+scope.byStep), scope.actif));
+                     if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+(1 / Math.pow(10, scope.byStep*-1))),scope.actif));
+                     if (scope.callback)  scope.callback (scope.value[scope.actif], scope.ctrlhandle);
+                     break;
+                case 37: // left
+                case 40: // down
+                    if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - scope.byStep), scope.actif));
+                    if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - (1 / Math.pow(10, scope.byStep*-1))),scope.actif));
+                    if (scope.callback)  scope.callback (scope.value[scope.actif], scope.ctrlhandle);
+                    break;
+                case 27: // esc
+                    scope.handles[scope.actif][0].blur();
+            }
+        };
+
+        scope.moveHandle = function (handle, clientX, clientY) {
+            var offset;
+            if (scope.vertical) {
+                offset = scope.bounds.bar.getBoundingClientRect().bottom - clientY;
+                if (offset > scope.bounds.bar.getBoundingClientRect().height) offset = scope.bounds.bar.getBoundingClientRect().height;
+                if (offset < scope.bounds.handles[handle].getBoundingClientRect().height) offset = scope.bounds.handles[handle].getBoundingClientRect().height;
+            } else {
+                offset = clientX - scope.bounds.bar.getBoundingClientRect().left;
+
+                if (offset < 0) offset = 0;
+                if ((clientX + scope.bounds.handles[handle].getBoundingClientRect().width) > scope.bounds.bar.getBoundingClientRect().right) {
+                    offset = scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width;
+                }
+            }
+
+            scope.getValue  (offset, handle);
+
+            // prevent dual handle to cross
+            if (scope.dual && scope.value [0] > scope.value[1]) {
+                if (handle === 0) scope.setValue (scope.value[0] , 1);
+                else scope.setValue(scope.value[1],0);
+            }
+        };
+
+
+        scope.focusCB = function (inside) {
+            if (inside) {
+                $document.on('keydown',scope.keydown);
+            } else {
+                $document.unbind('keydown',scope.keydown);
+            }
+        };
+
+        // bar was touch let move handle to this point
+        scope.touchBarCB = function (event) {
+            var handle=0;
+            var relative;
+            var touches = event.changedTouches;
+            var oldvalue = scope.value[handle];
+
+            event.preventDefault();
+
+            // if we have two handles select closest one from touch point
+            if (scope.dual) {
+                if (scope.vertical) relative = (touches[0].pageY - scope.bounds.bar.getBoundingClientRect().bottom) / scope.bounds.bar.getBoundingClientRect().height;
+                else relative= (touches[0].pageX - scope.bounds.bar.getBoundingClientRect().left) / scope.bounds.bar.getBoundingClientRect().width;
+
+                var distance0 = Math.abs(relative - scope.relative[0]);
+                var distance1 = Math.abs(relative - scope.relative[1]);
+                if (distance1 < distance0) handle=1;
+            }
+
+            // move handle to new place
+            scope.moveHandle (handle,touches[0].pageX, touches[0].pageY);
+            if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
+        };
+
+        // handle was touch and drag
+        scope.touchHandleCB = function (touchevt, handle) {
+            var oldvalue = scope.value[handle];
+
+            touchevt.preventDefault();
+            $document.on('touchmove',touchmove);
+            $document.on('touchend' ,touchend);
+            element.unbind('touchstart', scope.touchBarCB);
+
+            function touchmove(event) {
+                event.preventDefault();
+                var touches = event.changedTouches;
+                for (var idx = 0; idx < touches.length; idx++) {
+                    scope.moveHandle (handle,touches[idx].pageX, touches[idx].pageY);
+                }
+            }
+
+            function touchend(event) {
+               $document.unbind('touchmove',touchmove);
+               $document.unbind('touchend' ,touchend);
+               element.on('touchstart', scope.touchBarCB);
+
+                // if value change notify application callback
+                if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
+            }
+        };
+
+        scope.handleCB = function (clickevent, handle) {
+
+            if (attrs.automatic) return;
+            
+            var oldvalue = scope.value[handle];
+            // register mouse event to track handle
+            clickevent.preventDefault();
+
+            $document.on('mousemove',mousemove);
+            $document.on('mouseup', mouseup);
+            scope.handles[handle][0].focus();
+            scope.actif=handle;
+
+            // slider handle is moving
+            function mousemove(event) {
+                scope.moveHandle (handle, event.clientX, event.clientY);
+            }
+
+            // mouse is up dans leave slider send resize events
+            function mouseup() {
+                $document.unbind('mousemove', mousemove);
+                $document.unbind('mouseup', mouseup);
+
+                // if value change notify application callback
+                if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
+            }
+        };
+
+        // simulate jquery find by classes capabilities [warning only return 1st elements]
+        scope.find = function (select, elem) {
+            var domelem;
+
+            if (elem) domelem = elem[0].querySelector(select);
+            else domelem = element[0].querySelector(select);
+
+            var angelem = angular.element(domelem);
+            return (angelem);
+        };
+
+
+
+        scope.initialSettings = function (initial) {
+            var decimal_places_match_result;
+            scope.value=[];  // store low/height value when two handles
+            scope.relative=[];
+
+            if (scope.precision === null) {
+                decimal_places_match_result = ('' + scope.byStep).match(/\.([\d]*)/);
+                scope.precision = decimal_places_match_result && decimal_places_match_result[1] ? decimal_places_match_result[1].length : 0;
+            }
+
+            // position handle to initial value(s)
+            element.on('touchstart', scope.touchBarCB);
+            scope.handles[0].on('touchstart', function(evt){scope.touchHandleCB(evt,0);});
+
+            // this slider has two handles low/hight
+            if (scope.dual) {
+                scope.handles[1].addClass('range-slider-handle');
+                scope.handles[1].on('touchstart', function(evt){scope.touchHandleCB(evt,1);});
+                if (!scope.initvalues) scope.setValue (initial[1],1);
+            }
+
+            // if we have an initstate object apply it
+            if (scope.initvalues) scope.initWidget (scope.initvalues);
+            else   scope.setValue (initial[0],0);
+        };
+
+        scope.init = function () {
+            scope.sliderid   = attrs.id || "slider-" + parseInt (Math.random() * 1000);
+            scope.startValue = -Infinity;
+            scope.stopValue  = Infinity;
+            scope.byStep   = parseInt(attrs.byStep) || 1;
+            scope.vertical = attrs.vertical   || false;
+            scope.dual     = attrs.dualHandles|| false;
+            scope.trigger_input_change= false;
+            scope.notMore  = parseInt(attrs.notMore)   || 100;
+            scope.notLess  = parseInt(attrs.notLess)   || 0;
+
+            if (scope.vertical) element.addClass("vertical-range");
+
+            scope.handles= [scope.find('.handle-min'), scope.find('.handle-max')];
+            scope.bar    = element;
+            scope.slider = scope.find('.range-slider-active-segment');
+            scope.start  = scope.find('.ibz-range-slider-start');
+            scope.stop   = scope.find('.ibz-range-slider-stop');
+            scope.disable= attrs.disable || false;
+
+            scope.ctrlhandle = new RangeSliderHandle (scope);
+
+            // prepare DOM object pointer to compute size dynamically
+            scope.bounds = {
+                bar    : element[0],
+                handles: [scope.handles[0][0], scope.handles[1][0]]
+            };
+
+            if (attrs.disable === 'true') scope.setDisable(true);
+
+            if (attrs.displayTarget) {
+                switch (attrs.displayTarget) {
+                    case true :
+                    case 'handle' :
+                        scope.displays = scope.handles;
+                        scope.handles[0].addClass('ibz-range-slider-display');
+                        if (scope.dual) scope.handles[1].addClass('ibz-range-slider-display');
+                        break;
+                    default:
+                        scope.displays =  [$document.getElementById (attrs.displayTarget)];
+                }
+            } else scope.displays=[];
+
+            // extract initial values from attrs and parse into int
+            if (!attrs.initial) {
+                scope.initial  = [scope.ngModel, scope.ngModel]; // initialize to model values
+            } else {
+                var initial  = attrs.initial.split(',');
+                scope.initial = [
+                    initial[0] !== undefined ? parseInt (initial[0]) : scope.notLess,
+                    initial[1] !== undefined ? parseInt (initial[1]) : scope.notMore
+                ];
+            }
+
+            // Monitor any changes on start/stop dates.
+            scope.$watch('startAt', function() {
+                if (scope.value < scope.startAt ) {
+                    //scope.setValue (scope.startAt);
+                }
+                if (scope.startAt) scope.setStart (scope.startAt);
+            });
+
+            scope.$watch('stopAt' , function() {
+                if (scope.value > scope.stopAt) {
+                    //scope.setValue (scope.stopAt);
+                }
+                if (scope.stopAt) scope.setStop (scope.stopAt);
+            });
+
+            // finish widget initialisation
+            scope.initialSettings (scope.initial);
+
+        };
+
+        scope.init();
+        
+         // slider is ready provide control handle to application controller
+        scope.$watch ('inithook', function () {         // init Values may arrive late
+            if (scope.inithook) scope.inithook (scope.ctrlhandle);
+        });
+
+        scope.$watch ('initvalues', function () {      // init Values may arrive late
+            if (scope.initvalues) scope.initWidget(scope.initvalues);
+        });
+
+        // two-way binding if model value changes
+        scope.$watch ('ngModel', function (newValue) {
+          scope.setValue(newValue, 0);
+        });
+    }
+
+return {
+    restrict: "E",    // restrict to <range-slider> HTML element name
+    scope: {
+        startAt  :'=',  // First acceptable date
+        stopAt   :'=',  // Last acceptable date
+        callback :'=',  // Callback to actif when a date is selected
+        formatter:'=',  // Callback for drag event call each time internal value changes
+        inithook :'=',  // Hook point to control slider from API
+        cbhandle :'=',  // Argument added to every callback
+        initvalues:'=',   // Initial values as a single object
+        ngModel: '='    // the model value
+    },
+    require: '?ngModel',
+    template: template, // html template is build from JS
+    replace: true,      // replace current directive with template while inheriting of class
+    link: link          // pickadate object's methods
+};
+});
+
+console.log ("RangeSlider Loaded");
+
+})();
\ No newline at end of file
diff --git a/afm-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss b/afm-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss
new file mode 100644 (file)
index 0000000..6717d0e
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * ibz-Datepicker for Foundation
+ *
+ * Author: Fulup Ar Foll
+ * Date  : March-2015
+ * Object: SASS stylesheet, customized to Foundation
+ * References: https://css-tricks.com/stripes-css/
+ *
+ */
+@import "app/ibz-mixins";
+
+.range-slider-handle {
+  display: inline-block;
+  position: absolute;
+  z-index: 1;
+  top: -0.2rem;
+  width: 2rem;
+  height: 1.375rem;
+  border: 1px solid none;
+  cursor: pointer;
+  background: #008cba;
+}
+
+.range-slider.radius, .range-slider-handle {
+  background: #008cba;
+  -webkit-border-radius: 3px;
+  border-radius: 3px;
+}
+
+.range-slider-active-segment {
+  display: inline-block;
+  top: 0.07rem;
+  position: absolute;
+  height: 0.80rem;
+  background: #e5e5e5;
+}
+
+.ibz-range-slider {
+   background-color: rgba(154, 205, 50, 0.6) !important;
+   height: 1rem;
+   position: relative;
+   
+  .range-slider-active-segment {
+    background-color: rgba(82, 168, 200, 0.6);
+  }
+  
+  &-display {
+    background-color: rgba(82, 168, 200, 0.6) !important;
+    width : 4rem !important;
+    padding: .25rem;
+    text-align:center
+   }
+
+   &-start,&-stop {
+   display: inline-block;
+   position: absolute;
+   padding-top: 2px;
+   height: 95%;
+   background: repeating-linear-gradient(
+    45deg,
+    #606dbc,
+    #606dbc 10px,
+    #465298 10px,
+    #465298 20px
+  );}
+
+}
diff --git a/afm-client/app/etc/AppDefaults.js b/afm-client/app/etc/AppDefaults.js
new file mode 100644 (file)
index 0000000..b0eb1a8
--- /dev/null
@@ -0,0 +1,43 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var SESSION_TIMEOUT= 3600000; // default is 1h loggin session
+
+// Default config will be superseaded by ProjectRoot/.config-l4a.js $HOME/.config-l4a.js /etc/default/config-l4a.js
+config = {
+        
+    APPNAME : 'AFBclient',   // AppName is use as main Angular Module name
+    FRONTEND: "Frontend",    // HTML5 frontend  [no leading ./]
+    BACKEND : "Backend",     // NodeJS Rest API [no leading ./]
+    URLBASE : '/opa/',       // HTML basedir when running in production [should end with a /]
+    APIBASE : '/api/',       // Api url base dir [should end with a /]
+    DEBUG   : 4001,          // Node Debug Port
+    DBG_LVL : 5,             // Debug Trace Level 0=no trace.
+    
+    UPLOAD_DIR: '/tmp/uploads',  // directory destination for uploaded files [/api/post/upload]
+    
+    // EXPRESS WEB server config [note: URLBASE generate rewriting rules]
+    EXPRESS_HOST    : 'localhost',         // HTTP will only listen on related Internet interface
+    EXPRESS_PORT    : 4000,                // HTTP port
+    EXPRESS_LOGDIR  : __dirname + '/../../log',  // httpd log file
+    EXPRESS_SECRET  : Math.random().toString(36).slice(2), // [default cookie session]
+    EXPRESS_SESSION : SESSION_TIMEOUT
+};
+
+module.exports = config;
+
diff --git a/afm-client/app/etc/_Config.js b/afm-client/app/etc/_Config.js
new file mode 100644 (file)
index 0000000..ce93d43
--- /dev/null
@@ -0,0 +1,44 @@
+/* 
+ * Copyright 2014 Fulup Ar Foll
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fs = require('fs');
+
+function Config () {
+   'use strict';
+   var values=[];
+   var extention='-l4a.js';
+   var conf;
+
+   // Configs file path last one supersead first one.
+   var files= [__dirname + "/AppDefaults.js", "/etc/default/noderc"+ extention, process.env.NODERC, process.env.HOME + "/.noderc"+ extention , __dirname +"/../../.noderc.js" ];
+
+   // Parse any existing files within config list & merge them
+   for (var idx in files) { 
+      if (files[idx]) {
+        //console.log ("files=", files[idx]);  
+        if (fs.existsSync (files[idx])) conf=require (files[idx]);
+        for (var i in conf) values[i] = conf[i];
+      }     
+   }
+   
+ // set path to search for node_module within parent directory
+ process.env.NODE_PATH= process.env.NODE_PATH + '../node_modules';
+   
+ // console.log ("values=", values);
+ return values;
+}
+
+module.exports = Config();
diff --git a/afm-client/app/etc/_Trace.js b/afm-client/app/etc/_Trace.js
new file mode 100644 (file)
index 0000000..79ef4f5
--- /dev/null
@@ -0,0 +1,55 @@
+/* 
+ * Copyright 2014 Fulup Ar Foll
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var util       = require("util");
+var path       = require("path");
+var config= require('./_Config');
+
+function TracePoint () {
+        var saved = Error.prepareStackTrace;                           // save default prepareStack function
+        Error.prepareStackTrace = function(_, stack){ return stack; }; // overload err stack handling
+        Error.captureStackTrace(this, arguments.callee);               // request a stack
+        this.trace = this.stack;                                       // effectively build trace
+        Error.prepareStackTrace = saved;                               // restore original nodejs function  
+}
+
+// ------- Public Methods --------------
+var dbgLevel = function(target, level, format) {  //+ arguments
+    // try to get debugLevel from calling object or global config
+    if (target && target.dbgLevel)  dbgLevel = target.dbgLevel;
+    else dbgLevel = config.DBG_LVL || 1;
+    
+    if (dbgLevel >= level ) {
+
+        var args = [].slice.call(arguments, 2); // copy argument in a real array leaving out level
+        var message = util.format.apply(null, args);
+        
+        var trace = new TracePoint().trace;
+        var info = {
+            fullpath : trace[1].getFileName(),
+            linenum  : trace[1].getLineNumber(),
+            basename : path.basename (trace[1].getFileName())
+        };
+        
+        if (dbgLevel >= 5) {
+            console.log("%s:%d", info.fullpath, info.linenum);
+            console.log("\t[%d] %j", dbgLevel, message);
+        }
+        else console.log("--%d-- [%s:%d] -- %j", dbgLevel, info.basename, info.linenum, message);
+    }
+};
+
+module.exports = dbgLevel;
diff --git a/afm-client/bower.json b/afm-client/bower.json
new file mode 100644 (file)
index 0000000..c78c624
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "name": "healthy-gulp-angular",
+  "version": "0.0.0",
+  "authors": "",
+  "private": true,
+  "dependencies": {
+    "angular": "~1.3.4",
+    "angular-cookies": "~1.3.4",
+    "foundation-apps": "~1.1.0",
+    "angular-ui-notification": "~0.0.14",
+    "foundation-icon-fonts": "*",
+    "jszip": "Stuk/jszip#~2.5.0"
+  },
+  "overrides": {
+    "foundation": {
+      "main": [
+        "dist/js/foundation-apps-templates.js",
+        "dist/js/foundation-apps.js"
+      ]
+    }
+  }
+}
diff --git a/afm-client/gulpfile.js b/afm-client/gulpfile.js
new file mode 100644 (file)
index 0000000..3ec8774
--- /dev/null
@@ -0,0 +1,456 @@
+// BUG Symlink not working
+
+var gulp = require('gulp');
+var debug = require('gulp-debug');
+var plugins = require('gulp-load-plugins')();
+var del = require('del');
+var es = require('event-stream');
+var bowerFiles = require('main-bower-files');
+var print = require('gulp-print');
+var Q = require('q');
+var imagemin = require('gulp-imagemin'), pngquant = require('imagemin-pngquant');
+var taskListing = require('gulp-task-listing');
+var symlink = require('gulp-sym');
+var rename = require("gulp-rename");
+
+// addon for Foundation6
+var router   = require('front-router');
+
+// == PATH STRINGS ========
+var appdir  = "./app/";   // Warning to not forget trailling '/'
+config=require (appdir + "etc/_Config"); // upload user local preferences if any
+
+// Run node in debug mode in developpement mode ?
+var nodeopts = config.DEBUG !== undefined ? '--debug='+config.DEBUG : ''; 
+var frontend= appdir + config.FRONTEND;
+var backend = appdir + config.BACKEND;
+
+var paths = {
+    application : frontend,
+    scripts     : frontend+'/**/*.js',
+    appStyles   : [frontend+'/**/*.scss', '!'+frontend+'/styles/*/*-conf.scss'],
+    globalStyles: [frontend+'/styles/**/*.scss'],
+    images      : [frontend+'/**/*.png',frontend+'/**/*.jpg',frontend+'/**/*.jpeg',frontend+'/**/*.svg',frontend+'/**/*.ttf'],
+    index       : frontend+'/index.html',
+    partials    : [frontend + '/**/*.html', '!' + frontend +'/index.html'],
+    distDev     : './dist.dev',
+    distProd    : './dist.prod',
+    scriptsDevServer: backend + '/**/*.js',
+    sass:  [frontend+'/styles', 'bower_components/foundation-apps/scss','bower_components/foundation-icon-fonts'],
+    fonts: ['bower_components/**/*.woff'],
+    favicon: frontend+'/favicon.ico'
+};
+
+paths['distAppDev']  = paths.distDev + config.URLBASE;
+paths['distAppProd'] = paths.distProd + config.URLBASE;
+
+// Run node in debug mode in developpement mode ?
+var nodeopts = config.DEBUG !== undefined ? '--debug='+config.DEBUG : ''; 
+
+// == PIPE SEGMENTS ========
+var pipes = {};
+
+pipes.orderedVendorScripts = function() {
+    return plugins.order(['jquery.js', 'angular.js']);
+};
+
+pipes.orderedAppScripts = function() {
+    return plugins.angularFilesort();
+};
+
+pipes.minifiedFileName = function() {
+    return plugins.rename(function (path) {
+        path.extname = '.min' + path.extname;
+    });
+};
+
+pipes.validatedAppScripts = function() {
+    return gulp.src(paths.scripts)
+        .pipe(plugins.replace('@@APPNAME@@', config.APPNAME))
+        .pipe(plugins.jshint())
+        .pipe(plugins.jshint.reporter('jshint-stylish'));
+};
+
+pipes.builtAppScriptsDev = function() {
+    return pipes.validatedAppScripts()
+        .pipe(gulp.dest(paths.distAppDev));
+};
+
+pipes.builtAppScriptsProd = function() {
+    var scriptedPartials = pipes.scriptedPartials();
+    var validatedAppScripts = pipes.validatedAppScripts();
+
+    return es.merge(scriptedPartials, validatedAppScripts)
+        .pipe(plugins.ngAnnotate())
+        .pipe(pipes.orderedAppScripts())
+        .pipe(plugins.sourcemaps.init())
+        .pipe(plugins.concat(config.APPNAME+'.min.js'))
+        .pipe(plugins.uglify({compress: {drop_console: true}}))
+        .pipe(plugins.sourcemaps.write())
+        .pipe(gulp.dest(paths.distAppProd));
+};
+
+pipes.builtVendorScriptsDev = function() {
+    return gulp.src(bowerFiles())
+        .pipe(gulp.dest( paths.distDev +'/bower_components'));
+};
+
+pipes.builtVendorScriptsProd = function() {
+    return gulp.src(bowerFiles('**/*.js'))
+        .pipe(pipes.orderedVendorScripts())
+        .pipe(plugins.concat('vendor.min.js'))
+        .pipe(plugins.uglify())
+        .pipe(gulp.dest(paths.distProd+ '/bower_components'));
+};
+
+pipes.validatedDevServerScripts = function() {
+    return gulp.src(paths.scriptsDevServer)
+        .pipe(plugins.jshint())
+        .pipe(plugins.jshint.reporter('jshint-stylish'));
+};
+
+pipes.validatedPartials = function() {
+    return gulp.src(paths.partials)
+        .pipe(plugins.htmlhint({'doctype-first': false}))
+        .pipe(router({path: paths.application+'/etc/routes.js', root: paths.application}))
+        .pipe(plugins.htmlhint.reporter());
+};
+
+pipes.builtPartialsDev = function() {
+    return pipes.validatedPartials()
+        .pipe(gulp.dest(paths.distAppDev));
+};
+
+pipes.scriptedPartials = function() {
+    return pipes.validatedPartials()
+        .pipe(plugins.htmlhint.failReporter())
+        .pipe(plugins.htmlmin({collapseWhitespace: true, removeComments: true}))
+        .pipe(plugins.ngHtml2js({
+            moduleName: config.APPNAME,
+            template: "(function() {"
+               + "angular.module('<%= moduleName %>').run(['$templateCache', function($templateCache) {"
+               + "$templateCache.put('<%= template.url %>',\n    '<%= template.escapedContent %>');"
+               + "}]);\n"
+               + "})();\n"
+        }));    
+};
+
+pipes.builtAppStylesDev = function() {
+    return gulp.src(paths.appStyles)
+        .pipe(plugins.sass({includePaths: paths.sass}))
+        .pipe(gulp.dest(paths.distAppDev + '/styles'));
+};
+
+pipes.builtglobalStylesDev = function() {
+    return gulp.src(paths.globalStyles)
+        .pipe(plugins.sass({includePaths: paths.sass}))
+        .pipe(gulp.dest(paths.distDev  + '/global_styles'));
+};
+
+pipes.builtAppStylesProd = function() {
+    return gulp.src(paths.appStyles)
+        .pipe(plugins.sourcemaps.init())
+        .pipe(plugins.sass({includePaths: frontend + '/styles'}))
+        // .pipe(debug({title: '***** appStyle:'}))
+        .pipe(plugins.minifyCss())
+        .pipe(plugins.concat(config.APPNAME+'.css'))
+        .pipe(plugins.sourcemaps.write())
+        .pipe(pipes.minifiedFileName())
+        .pipe(gulp.dest(paths.distAppProd));
+};
+
+pipes.builtglobalStylesProd = function() {
+    return gulp.src(paths.globalStyles)
+        .pipe(plugins.sourcemaps.init())
+        .pipe(plugins.sass({includePaths: paths.sass}))
+        .pipe(plugins.minifyCss())
+        .pipe(plugins.sourcemaps.write())
+        .pipe(pipes.minifiedFileName())
+        .pipe(rename(function (path) {path.dirname="";return path;}))
+        .pipe(gulp.dest(paths.distProd + '/global_styles'));
+};
+
+pipes.processedFontsDev = function() {
+    return gulp.src(paths.fonts)
+        .pipe(rename(function (path) {path.dirname="";return path;}))
+        .pipe(gulp.dest(paths.distDev+'/bower_components'));
+};
+
+pipes.processedFontsProd = function() {
+    return gulp.src(paths.fonts)
+        .pipe(rename(function (path) {path.dirname="";return path;}))
+        .pipe(gulp.dest(paths.distProd+'/bower_components'));
+};
+
+
+pipes.processedImagesDev = function() {
+    return gulp.src(paths.images)
+        .pipe(gulp.dest(paths.distAppDev));
+};
+
+pipes.processedFaviconDev = function() {
+    return gulp.src(paths.favicon)
+        .pipe(gulp.dest(paths.distDev));
+};
+
+pipes.processedImagesProd = function() {
+    return gulp.src(paths.images)
+       .pipe(imagemin({
+            progressive: true,
+            svgoPlugins: [{removeViewBox: false}],
+            use: [pngquant()]
+        }))
+        .pipe(gulp.dest(paths.distAppProd));
+};
+
+pipes.processedFaviconProd = function() {
+    return gulp.src(paths.favicon)
+        .pipe(gulp.dest(paths.distProd));
+};
+
+// Create an Symlink when config.URLBASE exist
+pipes.createDevSymLink = function() {
+    return gulp.src(paths.distDev).pipe(symlink(paths.distDev+config.URLBASE, {force: true}));
+};
+
+pipes.createProdSymLink = function() {
+    return gulp.src(paths.distProd).pipe(symlink(paths.distDev+config.URLBASE,{force: true}));
+};
+
+pipes.validatedIndex = function() {
+    return gulp.src(paths.index)       
+        .pipe(plugins.replace('@@APPNAME@@', config.APPNAME))
+        .pipe(plugins.replace('@@URLBASE@@', config.URLBASE))
+        .pipe(plugins.htmlhint())
+        .pipe(plugins.htmlhint.reporter());
+};
+
+pipes.builtIndexDev = function() {
+
+    var orderedVendorScripts = pipes.builtVendorScriptsDev()
+        .pipe(pipes.orderedVendorScripts());
+
+    var orderedAppScripts = pipes.builtAppScriptsDev()
+        .pipe(pipes.orderedAppScripts());
+
+    var appStyles    = pipes.builtAppStylesDev();
+    var globalStyles = pipes.builtglobalStylesDev();
+
+    return pipes.validatedIndex()
+         // Vendor and Global should have absolute path to rootdir application one are relative to BaseURL
+        .pipe(plugins.inject(orderedVendorScripts, {relative: false, ignorePath: "/dist.dev", name: 'bower'}))
+        .pipe(plugins.inject(globalStyles, {relative: false, ignorePath: "/dist.dev", name:'vendor'}))
+        .pipe(gulp.dest(paths.distAppDev)) // write first to get relative path for inject
+        .pipe(plugins.inject(orderedAppScripts, {relative: true}))
+        .pipe(plugins.inject(appStyles, {relative: true, name: 'appli'}))
+        .pipe(gulp.dest(paths.distAppDev));
+};
+
+pipes.builtIndexProd = function() {
+
+    var vendorScripts= pipes.builtVendorScriptsProd();
+    var appScripts   = pipes.builtAppScriptsProd();
+    var appStyles    = pipes.builtAppStylesProd();
+    var globalStyles = pipes.builtglobalStylesProd();
+
+    return pipes.validatedIndex()
+         // Vendor and Global should have absolute path to rootdir application one are relative to BaseURL
+        .pipe(plugins.inject(vendorScripts, {relative: false, ignorePath: "/dist.prod", name: 'bower'}))
+        .pipe(plugins.inject(globalStyles, {relative: false, ignorePath: "/dist.prod", name:'vendor'}))
+        .pipe(gulp.dest(paths.distAppProd)) // write first to get relative path for inject
+        .pipe(plugins.inject(appScripts, {relative: true}))
+        .pipe(plugins.inject(appStyles, {relative: true, name:'appli'}))
+        .pipe(plugins.htmlmin({collapseWhitespace: true, removeComments: true}))
+        .pipe(gulp.dest(paths.distAppProd));
+};
+
+pipes.builtAppDev = function() {
+    return es.merge(pipes.builtIndexDev(), pipes.builtPartialsDev(), pipes.processedFaviconDev(), pipes.processedImagesDev(), pipes.processedFontsDev() );
+};
+
+pipes.builtAppProd = function() {
+    return es.merge(pipes.builtIndexProd(), pipes.processedFaviconProd(), pipes.processedImagesProd(), pipes.processedFontsProd());
+};
+
+
+// == TASKS ========
+
+// Add a task to render the output 
+gulp.task('help', taskListing.withFilters(/-/));
+   
+// clean, build of production environement
+gulp.task('build', ['clean-build-app-prod', 'validate-devserver-scripts']);
+
+gulp.task('run', function() {
+    // start nodemon to auto-reload the dev server
+    plugins.nodemon({ script: 'server.js', ext: 'js', watch: ['devServer/']})
+        .on('change', ['validate-devserver-scripts'])
+        .on('restart', function () {
+            console.log('[nodemon] restarted dev server');
+        });
+});       
+
+// removes all compiled dev files
+gulp.task('clean-dev', function() {
+    var deferred = Q.defer();
+    del(paths.distDev, function() {
+        deferred.resolve();
+    });
+    return deferred.promise;
+});
+
+// removes all compiled production files
+gulp.task('clean-prod', function() {
+    var deferred = Q.defer();
+    del(paths.distProd, function() {
+        deferred.resolve();
+    });
+    return deferred.promise;
+});
+
+// checks html source files for syntax errors
+gulp.task('validate-partials', pipes.validatedPartials);
+
+// checks index.html for syntax errors
+gulp.task('validate-index', pipes.validatedIndex);
+
+// moves html source files into the dev environment
+gulp.task('build-partials-dev', pipes.builtPartialsDev);
+
+// converts partials to javascript using html2js
+gulp.task('convert-partials-to-js', pipes.scriptedPartials);
+
+// runs jshint on the dev server scripts
+gulp.task('validate-devserver-scripts', pipes.validatedDevServerScripts);
+
+// runs jshint on the app scripts
+gulp.task('validate-app-scripts', pipes.validatedAppScripts);
+
+// moves app scripts into the dev environment
+gulp.task('build-app-scripts-dev', pipes.builtAppScriptsDev);
+
+// concatenates, uglifies, and moves app scripts and partials into the prod environment
+gulp.task('build-app-scripts-prod', pipes.builtAppScriptsProd);
+
+// compiles app sass and moves to the dev environment
+gulp.task('build-app-styles-dev', pipes.builtAppStylesDev);
+
+// compiles and minifies app sass to css and moves to the prod environment
+gulp.task('build-app-styles-prod', pipes.builtAppStylesProd);
+
+// moves vendor scripts into the dev environment
+gulp.task('build-vendor-scripts-dev', pipes.builtVendorScriptsDev);
+
+// concatenates, uglifies, and moves vendor scripts into the prod environment
+gulp.task('build-vendor-scripts-prod', pipes.builtVendorScriptsProd);
+
+// validates and injects sources into index.html and moves it to the dev environment
+gulp.task('build-index-dev', pipes.builtIndexDev);
+
+// validates and injects sources into index.html, minifies and moves it to the dev environment
+gulp.task('build-index-prod', pipes.builtIndexProd);
+
+// builds a complete dev environment
+gulp.task('build-app-dev', pipes.builtAppDev);
+
+// builds a complete prod environment
+gulp.task('build-app-prod', pipes.builtAppProd);
+
+// cleans and builds a complete dev environment
+gulp.task('clean-build-app-dev', ['clean-dev'], pipes.builtAppDev);
+
+// cleans and builds a complete prod environment
+gulp.task('clean-build-app-prod', ['clean-prod'], pipes.builtAppProd);
+
+// clean, build, and watch live changes to the dev environment
+gulp.task('watch-dev', ['clean-build-app-dev', 'validate-devserver-scripts'], function() {
+
+    // start nodemon to auto-reload the dev server
+    plugins.nodemon({  exec: 'node ' + nodeopts, script: backend+'/server.js', ext: 'js', watch: [backend], env: {NODE_ENV : 'dev'} })
+        .on('change', ['validate-devserver-scripts'])
+        .on('restart', function () {
+            console.log('[nodemon] restarted dev server');
+        });
+
+    // start live-reload server
+    plugins.livereload.listen({ start: true });
+
+    // watch index
+    gulp.watch(paths.index, function() {
+        return pipes.builtIndexDev()
+            .pipe(plugins.livereload());
+    });
+
+    // watch app scripts
+    gulp.watch(paths.scripts, function() {
+        return pipes.builtAppScriptsDev()
+            .pipe(plugins.livereload());
+    });
+
+    // watch html partials
+    gulp.watch(paths.partials, function() {
+        return pipes.builtPartialsDev()
+            .pipe(plugins.livereload());
+    });
+    
+    // watch Images
+    gulp.watch(paths.images, function() {
+        return pipes.processedImagesDev()
+            .pipe(plugins.livereload());
+    });
+
+    // watch styles
+    gulp.watch(paths.appStyles, function() {
+        return pipes.builtAppStylesDev()
+            .pipe(plugins.livereload());
+    });
+
+});
+
+// clean, build, and watch live changes to the prod environment
+gulp.task('watch-prod', ['clean-build-app-prod', 'validate-devserver-scripts'], function() {
+
+    // start nodemon to auto-reload the dev server
+    plugins.nodemon({ script: backend +'/server.js', ext: 'js', watch: [backend], env: {MODE : 'prod'} })
+        .on('change', ['validate-devserver-scripts'])
+        .on('restart', function () {
+            console.log('[nodemon] restarted dev server');
+        });
+
+    // start live-reload server
+    plugins.livereload.listen({start: true});
+
+    // watch index
+    gulp.watch(paths.index, function() {
+        return pipes.builtIndexProd()
+            .pipe(plugins.livereload());
+    });
+
+    // watch app scripts
+    gulp.watch(paths.scripts, function() {
+        return pipes.builtAppScriptsProd()
+            .pipe(plugins.livereload());
+    });
+
+    // watch hhtml partials
+    gulp.watch(paths.partials, function() {
+        return pipes.builtAppScriptsProd()
+            .pipe(plugins.livereload());
+    });
+    
+    // watch Images
+    gulp.watch(paths.images, function() {
+        return pipes.processedImagesProd()
+            .pipe(plugins.livereload());
+    });
+
+    // watch styles
+    gulp.watch(paths.appStyles, function() {
+        return pipes.builtAppStylesProd()
+            .pipe(plugins.livereload());
+    });
+    
+});
+
+// default task builds for prod
+gulp.task('default', ['clean-build-app-prod']);
diff --git a/afm-client/index.html b/afm-client/index.html
new file mode 100644 (file)
index 0000000..32e0efe
--- /dev/null
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--[if lt IE 7]>      <html lang="en" ng-app="AudioClient" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
+<!--[if IE 7]>         <html lang="en" ng-app="AudioClient" class="no-js lt-ie9 lt-ie8"> <![endif]-->
+<!--[if IE 8]>         <html lang="en" ng-app="AudioClient" class="no-js lt-ie9"> <![endif]-->
+<!--[if gt IE 8]><!--> <html lang="en" ng-app="AudioClient" class="no-js"> <!--<![endif]-->
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>Simple Sample Application</title>
+    <meta name="description" content="">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <!-- bower:css -->
+    <!-- endinject -->
+     <!--vendor:css -->
+    <!-- endinject -->
+    <!-- appli:css -->
+    <!-- endinject -->
+    <!-- inject:css -->
+    <!-- endinject -->
+    <base href="/opa/"> <!-- https://docs.angularjs.org/error/$location/nobase -->
+
+</head>
+<body>
+<!--[if lt IE 7]>
+<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
+<![endif]-->
+<div ui-view></div>
+<!-- bower:js -->
+<!-- endinject -->
+<!-- inject:js -->
+<!-- endinject -->
+
+<!-- Generic Foundation Modal Template -->
+<script id="components/modal/modal.html" type="text/ng-template">
+   <div  class="modal-overlay" ng-click="hideOverlay()">
+    <aside class="modal" ng-click="$event.stopPropagation();" ng-transclude></aside>
+  </div>
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/afm-client/nbproject/project.properties b/afm-client/nbproject/project.properties
new file mode 100644 (file)
index 0000000..770258f
--- /dev/null
@@ -0,0 +1,35 @@
+auxiliary.org-netbeans-modules-css-prep.less_2e_compiler_2e_options=
+auxiliary.org-netbeans-modules-css-prep.less_2e_enabled=false
+auxiliary.org-netbeans-modules-css-prep.less_2e_mappings=/less:/css
+auxiliary.org-netbeans-modules-css-prep.sass_2e_compiler_2e_options=
+auxiliary.org-netbeans-modules-css-prep.sass_2e_configured=true
+auxiliary.org-netbeans-modules-css-prep.sass_2e_enabled=false
+auxiliary.org-netbeans-modules-css-prep.sass_2e_mappings=/scss:/css
+auxiliary.org-netbeans-modules-javascript-gulp.action_2e_build=build-app-dev
+auxiliary.org-netbeans-modules-javascript-gulp.action_2e_clean=clean-dev
+auxiliary.org-netbeans-modules-javascript-gulp.action_2e_debug=watch-dev
+auxiliary.org-netbeans-modules-javascript-gulp.action_2e_rebuild=clean-build-app-dev
+auxiliary.org-netbeans-modules-javascript-gulp.action_2e_run=watch-prod
+auxiliary.org-netbeans-modules-javascript-nodejs.enabled=true
+auxiliary.org-netbeans-modules-javascript-nodejs.node_2e_default=true
+auxiliary.org-netbeans-modules-javascript-nodejs.run_2e_enabled=true
+auxiliary.org-netbeans-modules-javascript-nodejs.run_2e_restart=false
+auxiliary.org-netbeans-modules-javascript-nodejs.start_2e_args=watch-dev
+auxiliary.org-netbeans-modules-javascript-nodejs.start_2e_file=node_modules/.bin/gulp
+auxiliary.org-netbeans-modules-javascript-nodejs.sync_2e_enabled=true
+auxiliary.org-netbeans-modules-javascript2-requirejs.enabled=true
+auxiliary.org-netbeans-modules-web-clientproject-api.js_2e_libs_2e_folder=js/libs
+browser.autorefresh.Chrome.INTEGRATED=true
+browser.highlightselection.Chrome.INTEGRATED=true
+browser.run=false
+file.reference.app-Frontend=app/Frontend
+file.reference.healthy-gulp-angular-foundation6-app=app
+file.reference.l4a-hypervisor-app=app
+file.reference.l4a-hypervisor-Frontend=Frontend
+files.encoding=UTF-8
+project.license=gpl30
+run.as=node.js
+site.root.folder=${file.reference.app-Frontend}
+source.folder=${file.reference.l4a-hypervisor-app}
+start.file=index.html
+web.context.root=/l4a-hypervisor
diff --git a/afm-client/nbproject/project.xml b/afm-client/nbproject/project.xml
new file mode 100644 (file)
index 0000000..0149ec5
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+    <type>org.netbeans.modules.web.clientproject</type>
+    <configuration>
+        <data xmlns="http://www.netbeans.org/ns/clientside-project/1">
+            <name>afm-client</name>
+        </data>
+        <libraries xmlns="http://www.netbeans.org/ns/cdnjs-libraries/1"/>
+    </configuration>
+</project>
diff --git a/afm-client/package.json b/afm-client/package.json
new file mode 100644 (file)
index 0000000..0964af8
--- /dev/null
@@ -0,0 +1,61 @@
+{
+  "name": "afm-client",
+  "private": true,
+  "version": "0.0.1",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/iotbzh/healthy-gulp-angular-foundation6"
+  },
+  "scripts": {
+    "postinstall": "bower install",
+    "build": "./node_modules/.bin/gulp clean-build-app-dev",
+    "start": "node node_modules/.bin/gulp watch-dev"
+  },
+  "devDependencies": {
+    "body-parser": "^1.5.2",
+    "bower": "^1.3.1",
+    "cookie-parser": "^1.4.0",
+    "del": "^1.1.1",
+    "event-stream": "^3.2.2",
+    "express": "^4.7.2",
+    "express-session": "^1.12.1",
+    "front-router": "^1.0.0",
+    "gulp": "^3.9.0",
+    "gulp-angular-filesort": "^1.0.4",
+    "gulp-debug": "^2.1.2",
+    "gulp-concat": "^2.4.3",
+    "gulp-htmlhint": "0.0.9",
+    "gulp-htmlmin": "^1.0.0",
+    "gulp-imagemin": "*",
+    "gulp-inject": "^1.1.1",
+    "gulp-jshint": "^1.9.2",
+    "gulp-livereload": "^3.7.0",
+    "gulp-load-plugins": "^0.8.0",
+    "gulp-minify-css": "^0.4.5",
+    "gulp-ng-annotate": "^1.1.0",
+    "gulp-ng-html2js": "^0.2.0",
+    "gulp-nodemon": "^2.0.0",
+    "gulp-order": "^1.1.1",
+    "gulp-print": "^1.1.0",
+    "gulp-rename": "^1.2.0",
+    "gulp-replace": "^0.5.4",
+    "gulp-sass": "^2.0.3",
+    "gulp-sourcemaps": "^1.3.0",
+    "gulp-task-listing": "^1.0.1",
+    "gulp-uglify": "^1.2.0",
+    "imagemin-pngquant": "*",
+    "jshint-stylish": "^1.0.0",
+    "karma": "^0.10",
+    "karma-junit-reporter": "^0.2.2",
+    "main-bower-files": "^2.5.0",
+    "method-override": "^2.1.2",
+    "protractor": "^1.1.1",
+    "q": "^1.1.2",
+    "shelljs": "^0.2.6",
+    "traceback": "^0.3.1",
+    "gulp-sym": "0.0.14"
+  },
+  "dependencies": {
+    "multer": "^1.1.0"
+  }
+}