diff --git a/.gitignore b/.gitignore index bfec715..ecc8824 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .sass-cache .vagrant -node_modules \ No newline at end of file +node_modules +npm-debug.log \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6770908 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: node_js +node_js: + - "5" + - "5.1" + - "4" + - "4.2" + - "4.1" + - "4.0" + - "0.12" + - "0.11" + - "0.10" +before_install: + - npm install -g grunt-cli + - gem install sass +install: npm install +before_script: grunt \ No newline at end of file diff --git a/LICENSE b/LICENSE index aafdb1c..387f7e7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Stefan Cosma +Copyright (c) 2016 Stefan Cosma Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4e8e0d8..24de508 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,53 @@ -uptimey +uptimey ======= -Are you proud of your server uptime because you put a lot of time into configuring it? +[![Build Status](https://travis-ci.org/stefanbc/uptimey.svg?branch=dev)](https://travis-ci.org/stefanbc/uptimey) [![Dependency Status](https://www.versioneye.com/user/projects/572c7efaa0ca35004cf77288/badge.svg?style=flat)](https://www.versioneye.com/user/projects/572c7efaa0ca35004cf77288) -Showcase your server uptime with **uptimey** - a beautiful Server Uptime Monitor! +If you're proud of your server uptime, because you put a lot of time into configuring it, then you can showcase it with **uptimey** - a beautiful Server Uptime Monitor! -Just fork the repo on your web server and then access `/uptimey` in your browser. Simple as that! +Just clone the repo on your web server and then access your server's host followed by `/uptimey`, in your browser. Simple as that! -**Features** +Features +-- -* Background image is the Bing image of the day and it changes automatically! -* Works on Linux, Windows, Mac OS servers +* The background image is a random image from [Unsplash](http://unsplash.com)! +* Works on Linux, Windows, Mac OS servers. +* Automatically gathers data from the server. +* Knows if it's nighttime or daytime. +* Knows your aprox server location (based on IP). +* Tweet your awesome uptime! +* Screenshot the server uptime and show it to your devops buddies! :) +* Configure it to your liking. You can modify the `client/bin/settings.json` file. -![Screenshot](http://i.imgur.com/isg9t8n.png) +![Screenshot](https://i.imgur.com/BdIzEkg.png) -Made by [Stefan Cosma](http://coderbits.com/stefanbc) \ No newline at end of file +Requirements +-- + +* Apache server +* PHP +* Access to the Internet + +Developers +-- + +Make sure you have Node and npm installed. You'll need to have Grunt and Sass installed. Use these commands: + +``` +npm install -g grunt-cli +gem install sass +``` + +You can then install all the project dependencies using: + +``` +npm install +``` + +Available Grunt tasks: + +* `grunt` - will build the whole project. +* `grunt watch` - will watch for any file modifications and will build. Will also build on start. +* `grunt test` - will test the main app js file using `jshint` (more tests are comming soon). + +For local development you can use Vagrant and you can check if the build passes using Travis-CI. diff --git a/Vagrantfile b/Vagrantfile index 549d993..df77c06 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -6,6 +6,6 @@ VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = "hashicorp/precise32" - config.vm.provision :shell, path: "testing.sh" + config.vm.provision :shell, path: "vagrant-setup.sh" config.vm.network "forwarded_port", guest: 80, host: 8080 end \ No newline at end of file diff --git a/client/bin/app.min.js b/client/bin/app.min.js new file mode 100644 index 0000000..0218ef4 --- /dev/null +++ b/client/bin/app.min.js @@ -0,0 +1 @@ +(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r;p="https://github.com/stefanbc/uptimey",h="./client/lib/models/data.php",g="./client/bin/config.js",q="./client/bin/settings.json",m=require("moment"),j=require("humane"),a="",b="",c="",n=function(a){return j.log(a,{timeout:5e3,baseCls:"humane-libnotify"})},f=function(a){return $(a).addClass("pulse"),$(a).on("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){$(this).removeClass("pulse")})},l=function(a){return!isNaN(parseFloat(a))},i=function(a){var b,c;if("number"==typeof a||"boolean"==typeof a)return!1;if("undefined"==typeof a||null===a)return!0;if("undefined"!=typeof a.length)return 0===a.length;b=0;for(c in a)a.hasOwnProperty(c)&&b++;return 0===b},d=function(a){var b,c,d,e,g;switch(d="",a){case"toggle":d=$(".toggle-button").attr("data-status"),"closed"===d?($(".button-container").animate({top:0}),$(".toggle-button").attr("data-status","open"),$(".toggle-button").removeClass("fa-angle-double-down"),$(".toggle-button").addClass("fa-angle-double-up")):"open"===d&&($(".button-container").animate({top:"-80px"}),$(".toggle-button").attr("data-status","closed"),$(".toggle-button").removeClass("fa-angle-double-up"),$(".toggle-button").addClass("fa-angle-double-down"));break;case"advanced":f(".advanced-button"),d=$(".advanced-button").attr("data-status"),"default"===d?($(".advanced-button").attr("data-status","advanced"),$(".advanced-button").addClass("active"),$(".default-panel").fadeOut(500),$(".advanced-panel").fadeIn(500),$.ajax(h,{method:"GET",data:{action:"advanced",flag:"advanced"},success:function(a){$(".advanced-panel .top-container").html(a)}})):"advanced"===d&&($(".advanced-button").attr("data-status","default"),$(".advanced-button").removeClass("active"),$(".advanced-panel").fadeOut(500),$(".default-panel").fadeIn(500));break;case"refresh":$(".refresh-button").addClass("fa-spin"),o("uptime","refresh"),o("time","refresh"),o("ping"),setTimeout(function(){$(".refresh-button").removeClass("fa-spin")},1e3);break;case"twitter":f(".twitter-button"),g="",0!==$("#days").attr("data-value")&&(g+=$("#days").attr("data-value")+" days "),0!==$("#hours").attr("data-value")&&(g+=$("#hours").attr("data-value")+" hours "),0!==$("#minutes").attr("data-value")&&(g+=$("#minutes").attr("data-value")+" minutes"),e=g+" server uptime. Can you beat this? via",b="uptimey,devops",window.open("http://twitter.com/share?url="+p+"&text="+e+"&hashtags="+b+"&","twitterwindow","height=450, width=550, top="+($(window).height()/2-225)+", left="+$(window).width()/2+", toolbar=0, location=0, menubar=0, directories=0, scrollbars=0");break;case"google-plus":f(".google-plus-button"),n("Feature still in development");break;case"facebook":f(".facebook-button"),n("Feature still in development");break;case"screenshot":c=$(".screenshot-button"),f(c),c.hasClass("fa-camera")?(c.removeClass("fa-camera").addClass("fa-download"),html2canvas(document.body,{onrendered:function(a){var b,d;b=a.toDataURL("image/png").replace("image/png","image/octet-stream"),c.attr("href",b),d=m().format("DDMMYYYYHHmmss"),c.attr("download","Screenshot_"+d+".png")}})):setTimeout(function(){return c.removeClass("fa-download").addClass("fa-camera"),c.removeAttr("href").removeAttr("download")},3e3);break;case"clear":$.ajax(h,{method:"GET",data:{action:"clear"}})}},r=function(){var a;return a=document.createElement("style"),a.appendChild(document.createTextNode("")),document.head.appendChild(a),a.sheet}(),e=function(a,b,c){"insertRule"in a?a.insertRule(b+"{"+c+"}",a.cssRules.length):"addRule"in a&&a.addRule(b,c,a.cssRules.length)},g=function(){$.ajax(h,{method:"GET",data:{action:"override",flag:"override"},error:function(a){return console.log(a)},success:function(a){var b,c,f;g=$.parseJSON(a),i(g.background_color)||(b="background-color: "+g.background_color+";"),i(g.font_family)||(b+="font-family: "+g.font_family+";"),i(g.font_color)||(b+="color: "+g.font_color+";"),e(r,"body",b),$.each(g.buttons[0],function(a,b){var c;b===!1&&(c="display: none",e(r,"."+a+"-button",c))}),"advanced"===g.default_view&&d("advanced"),"top"!==g.menu_placement&&$(".button-container").removeClass("top-menu").addClass(g.menu_placement+"-menu"),g.remove_menu===!0&&(f="display: none",e(r,".button-container",f)),g.show_location===!1&&(c="display: none",e(r,".location-inner",c)),g.show_menu_always===!0&&d("toggle")}})},o=function(d,e){var f;switch(d){case"image":$.ajax(h,{method:"GET",data:{action:d},success:function(a){a=a.split(";"),$("body").css("backgroundImage","url("+a[0]+")")}}),f="Made with Uptimey. ",f+="Image from Unsplash.",$("#copy").html(f);break;case"location":$.ajax(h,{method:"GET",data:{action:d},success:function(d){var e;e="http://ipinfo.io/"+d,$.getJSON(e,function(d){var e;$("#location").text(d.city+", "+d.region+", "+d.country).addClass("fadeIn"),e=d.loc.split(","),$("#location").attr("data-latlong",e[0]+"+"+e[1]),a=d.city+", "+d.region+", "+d.country,$.simpleWeather({location:a,success:function(a){b=a.sunrise,c=a.sunset}})}),$(".location-inner").addClass("fadeIn")}});break;case"uptime":$.ajax(h,{method:"GET",data:{action:d,flag:e},success:function(a){a=a.split(";"),$("#days").text(a[0]).addClass("fadeIn"),$("#days").attr("data-value",a[0]),$("#hours").text(a[1]).addClass("fadeIn"),$("#hours").attr("data-value",a[1]),$("#minutes").text(a[2]).addClass("fadeIn"),$("#minutes").attr("data-value",a[2]),$(".bottom-container").addClass("fadeIn")}});break;case"time":$.ajax(h,{method:"GET",data:{action:d,flag:e},success:function(a){var d;a=a.split(";"),$("#current").text(a[0]).addClass("fadeIn"),d=a[1].split(":"),$("#time").html(d[0]+":"+d[1]).addClass("fadeIn"),$("#since").text(a[2]).addClass("fadeIn"),setTimeout(function(){var d,e,f;d=m(b,"h:m a").format("X"),e=m(c,"h:m a").format("X"),f=m(a[1],"h:m a").format("X"),f>=d&&e>=f?($(".time .fa").removeClass("fa-moon-o fa-circle-o"),$(".time .fa").addClass("fa-sun-o")):($(".time .fa").removeClass("fa-sun-o fa-circle-o"),$(".time .fa").addClass("fa-moon-o"))},3e3),$(".top-container").addClass("fadeIn")}});break;case"ping":$.ajax(h,{method:"GET",data:{action:d},error:function(a,b){return n(b)},success:function(a){return n(a)}})}$(".val").each(function(){$(this).on("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){$(this).removeClass("fadeIn")})})},k=function(){return g(),$(".top-container").addClass("animated"),$(".bottom-container").addClass("animated"),$(".val").addClass("animated"),$(".button").addClass("animated"),o("image"),o("location"),o("uptime"),o("time")},$(function(){k(),setInterval(function(){o("uptime"),o("time")},6e4),setInterval(function(){o("ping")},3e5),$(".button").each(function(){$(this).on("click",function(){var a;a=$(this).attr("data-action"),d(a)})}),$("#location").on("click",function(){var a;a=$(this).attr("data-latlong"),window.location.href="https://www.google.com/maps/place/"+a})})}).call(this); \ No newline at end of file diff --git a/client/bin/config.js b/client/bin/config.js new file mode 100644 index 0000000..3835832 --- /dev/null +++ b/client/bin/config.js @@ -0,0 +1,20 @@ +var config = { + production: {}, + development: { + url: '', + mail: { + transport: 'SMTP', + options: { + service: 'Mailgun', + auth: { + user: '', // mailgun username + pass: '' // mailgun password + } + } + }, + server: { + host: '127.0.0.1', + port: '8080' + } + } +} \ No newline at end of file diff --git a/client/bin/css/global.min.css b/client/bin/css/global.min.css new file mode 100644 index 0000000..aea440a --- /dev/null +++ b/client/bin/css/global.min.css @@ -0,0 +1 @@ +.button-container .button-block:after,.bottom-container section .col:first-child:before,.bottom-container section .col:nth-child(3):before{content:"";height:1px;background:-moz-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:-webkit-gradient(linear, left top, right top, color-stop(0%, transparent), color-stop(50%, #939393), color-stop(100%, transparent));background:-webkit-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:-o-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:-ms-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:linear-gradient(to right, transparent 0%, #939393 50%, transparent 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#00000000', endColorstr='#00000000',GradientType=1 );display:block}a,.button-container .button,.humane,.humane-libnotify{-webkit-transition:all 0.5s ease;-moz-transition:all 0.5s ease;-o-transition:all 0.5s ease;-ms-transition:all 0.5s ease;transition:all 0.5s ease}.notice,.button-container{left:50%;-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);-o-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}@-webkit-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@-moz-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@-ms-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@-o-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}html{height:100%}body{height:100%;background-color:#222;background-repeat:no-repeat;background-position:center;background-size:cover;font-family:Raleway,sans-serif;font-weight:200;color:#fff}a{color:#fff;text-decoration:none}a:hover,a:focus{text-decoration:underline}.blink{-webkit-animation:blink 2s infinite;-moz-animation:blink 2s infinite;-ms-animation:blink 2s infinite;-o-animation:blink 2s infinite;animation:blink 2s infinite}.notice{position:absolute;bottom:0;background:rgba(0,0,0,0.7);padding:35px}.container{background:rgba(0,0,0,0.6);height:100%;width:100%;position:relative}.button-container{position:absolute;top:-80px;text-align:center;font-size:1.5em;z-index:10}.button-container .button-block{margin-top:25px}.button-container .button-block:after{margin-top:25px}.button-container .button{cursor:pointer;margin:0 30px;position:relative}.button-container .button:after{content:attr(data-action);position:absolute;visibility:hidden;color:#fff;font-family:Raleway,sans-serif;font-weight:200;font-size:0.5em;line-height:30px;text-align:center;background:rgba(0,0,0,0.6);width:100px;height:30px;border-radius:6px}.button-container .button:hover:after{visibility:visible;opacity:1;top:35px;left:50%;margin-left:-50px;z-index:999}.button-container .refresh-button:hover{color:#00ac8a}.button-container .advanced-button.active{color:#e74b3b}.button-container .twitter-button:hover{color:#55acee}.button-container .google-plus-button:hover{color:#dd4b39}.button-container .facebook-button:hover{color:#3B5998}.button-container .screenshot-button{text-decoration:none}.button-container .screenshot-button:hover{color:#f7d61c}.advanced-panel{display:none}.top-container{margin:0 auto;position:absolute;bottom:400px;left:0;width:100%}.top-container section{width:960px;margin:0 auto;position:relative}.top-container section .row{display:block;margin-bottom:20px}.top-container section .row .val{display:block;font-size:2.5em;padding-bottom:10px}.top-container section .block-right{position:absolute;top:0;right:30px}.top-container section .time{padding:20px 0}.top-container section .time .fa{font-size:3em;margin-right:15px}.top-container section .time .fa-sun-o{color:#ffd900}.top-container section .time .fa-moon-o{color:#3498db}.top-container section .time .val{font-size:4em}.top-container section .location{top:-80px;font-size:1.5em;cursor:pointer}.top-container section .location:hover{text-decoration:underline}.top-container section .location .fa{margin-right:15px}.top-container h2{margin:0;font-weight:lighter;font-size:1.1em}.top-container .notif{width:960px;margin:0 auto;position:relative;display:block;font-family:FontAwesome,Raleway,sans-serif;font-weight:200}.top-container .notif:before{padding-right:10px}.top-container .notif a{text-decoration:underline}.bottom-container{text-align:center;position:absolute;bottom:150px;left:0;width:100%}.bottom-container section{width:960px;margin:0 auto}.bottom-container section .col{width:33%;display:inline-block;padding-top:15px}.bottom-container section .col:first-child:before{margin-bottom:25px}.bottom-container section .col:nth-child(3):before{margin-bottom:25px}.bottom-container section .col .val{display:block;font-size:6em;padding-bottom:10px}.bottom-container section .col .label{text-transform:capitalize}.bottom-container h2{font-weight:lighter;font-size:1.1em;margin-bottom:-25px}footer{position:absolute;bottom:10px;right:20px;color:#ddd;font-size:0.7em;text-align:right}footer a{text-decoration:underline}.humane,.humane-libnotify{color:#fff;font-family:Raleway,sans-serif;font-weight:200;font-size:0.8em;text-align:center;background:rgba(0,0,0,0.6);position:fixed;top:10px;right:10px;z-index:100000;opacity:0;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);width:165px;padding:10px;border-radius:6px;-moz-transform:translateY(-40px);-webkit-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}.humane.humane-animate,.humane-libnotify.humane-libnotify-animate{opacity:1;-moz-transform:translateY(0);-webkit-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}.humane.humane-animate,.humane-libnotify.humane-libnotify-js-animate{opacity:1;-moz-transform:translateY(0);-webkit-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}.humane.humane-libnotify-info{background:rgba(0,0,100,0.9)}.humane.humane-libnotify-success{background:rgba(0,100,0,0.9)}.humane.humane-libnotify-error{background:rgba(100,0,0,0.9)}.humane-libnotify.humane-libnotify-info{background:rgba(0,0,100,0.9)}.humane-libnotify.humane-libnotify-success{background:rgba(0,100,0,0.9)}.humane-libnotify.humane-libnotify-error{background:rgba(100,0,0,0.9)} diff --git a/client/bin/settings.json b/client/bin/settings.json new file mode 100644 index 0000000..a358cdb --- /dev/null +++ b/client/bin/settings.json @@ -0,0 +1,23 @@ +{ + "background_color" : "", + "background_image" : "", + "buttons": [{ + "refresh" : true, + "advanced" : true, + "twitter" : true, + "google-plus" : false, + "facebook" : false, + "screenshot" : true + }], + "debug_mode" : false, + "default_view" : "default", + "display_timezone" : "Europe/Bucharest", + "font_color" : "", + "font_family" : "", + "menu_placement" : "top", + "remove_menu" : false, + "show_am_pm" : true, + "show_location" : true, + "show_menu_always" : false, + "use_24h_clock" : false +} \ No newline at end of file diff --git a/client/lib/controllers/actions.coffee b/client/lib/controllers/actions.coffee new file mode 100644 index 0000000..aaa98e3 --- /dev/null +++ b/client/lib/controllers/actions.coffee @@ -0,0 +1,131 @@ +### Button action ### +action = (type) -> + status = '' + switch type + when 'toggle' + # Get the status of the button + status = $('.toggle-button').attr('data-status') + # Check the status + if status is 'closed' + # Animate the container (bring it down) + $('.button-container').animate top: 0 + # Change the button status + $('.toggle-button').attr 'data-status', 'open' + # Change the icon + $('.toggle-button').removeClass 'fa-angle-double-down' + $('.toggle-button').addClass 'fa-angle-double-up' + else if status is 'open' + # Animate the container (bring it up) + $('.button-container').animate top: '-80px' + # Change the button status + $('.toggle-button').attr 'data-status', 'closed' + # Change the icon + $('.toggle-button').removeClass 'fa-angle-double-up' + $('.toggle-button').addClass 'fa-angle-double-down' + return + when 'advanced' + # Animated it + animateElement '.advanced-button' + # Get the status of the button + status = $('.advanced-button').attr('data-status') + # Check the state + if status is 'default' + # Show the correct panel and set the button state + $('.advanced-button').attr 'data-status', 'advanced' + $('.advanced-button').addClass 'active' + $('.default-panel').fadeOut 500 + $('.advanced-panel').fadeIn 500 + # Get the data for this panel + $.ajax data, + method : 'GET' + data : + action : 'advanced', + flag : 'advanced' + success: (notice) -> + # Set the data from ajax + $('.advanced-panel .top-container').html notice + return + else if status is 'advanced' + # Show the correct panel and set the button state + $('.advanced-button').attr 'data-status', 'default' + $('.advanced-button').removeClass 'active' + $('.advanced-panel').fadeOut 500 + $('.default-panel').fadeIn 500 + return + when 'refresh' + # Animated it + $('.refresh-button').addClass 'fa-spin' + # Refresh the values + output 'uptime', 'refresh' + output 'time', 'refresh' + output 'ping' + # Stop animation after 1s + setTimeout (-> + $('.refresh-button').removeClass 'fa-spin' + return + ), 1000 + return + when 'twitter' + # Animated it + animateElement '.twitter-button' + # The action + # Get the current uptime + uptime = '' + if $('#days').attr('data-value') isnt 0 + uptime += $('#days').attr('data-value') + ' days ' + if $('#hours').attr('data-value') isnt 0 + uptime += $('#hours').attr('data-value') + ' hours ' + if $('#minutes').attr('data-value') isnt 0 + uptime += $('#minutes').attr('data-value') + ' minutes' + # Set the tweet + text = uptime + ' server uptime. Can you beat this? via' + # Set the hashtag + hashtag = 'uptimey,devops' + # Open the Twitter share window + window.open "http://twitter.com/share?url=#{projectLink}&text=#{text}&hashtags=#{hashtag}&", 'twitterwindow', "height=450, width=550, top=#{$(window).height() / 2 - 225}, left=#{$(window).width() / 2}, toolbar=0, location=0, menubar=0, directories=0, scrollbars=0" + return + when 'google-plus' + # Animated it + animateElement '.google-plus-button' + # The action + notice "Feature still in development" + return + when 'facebook' + # Animated it + animateElement '.facebook-button' + # The action + notice "Feature still in development" + return + when 'screenshot' + screenshotButton = $('.screenshot-button') + # Animated it + animateElement screenshotButton + # Check the button status + if screenshotButton.hasClass('fa-camera') + # Change the button icon + screenshotButton.removeClass('fa-camera').addClass 'fa-download' + # Create an image from canvas + html2canvas document.body, onrendered: (canvas) -> + # Save the canvas to a data URL + dataURL = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream') + # Set the data url on the screenshot button + screenshotButton.attr 'href', dataURL + # Get the current date for the filename + fileName = moment().format('DDMMYYYYHHmmss') + # Set the filename on the screenshot button + screenshotButton.attr 'download', 'Screenshot_' + fileName + '.png' + return + else + setTimeout (-> + # Change the button icon + screenshotButton.removeClass('fa-download').addClass 'fa-camera' + screenshotButton.removeAttr('href').removeAttr 'download' + ), 3000 + return + when 'clear' + # Clear the session + $.ajax data, + method : 'GET' + data : + action : 'clear' + return diff --git a/client/lib/controllers/config.coffee b/client/lib/controllers/config.coffee new file mode 100644 index 0000000..49973c4 --- /dev/null +++ b/client/lib/controllers/config.coffee @@ -0,0 +1,62 @@ +sheet = do -> + # Create the