Daily and smart (by session) dump

Daily and smart (by session) dump

Dump are now taking daily (or asap after 24 hours). The dump is composed
of user sessions which contains pages. The dump is then sent to a server
via a POST request. The address of the server is (for now) hardcoded at
the head of userStudyToolbox.coffee. Modification have been done also to
the regular dump to match the same json structure. The loading of
mem0r1es has been updating accordingly.

--- src.coffee/userStudyToolbox.coffee
+++ src.coffee/userStudyToolbox.coffee
@@ -4,8 +4,16 @@

   constructor : (@storageManager)->
     @currentCount = 0
+    @initLastDump()
+    @dumpServerURL = "http://127.0.0.1:8080/"
     @checkIfNeedNewContext()
+    @dumpDailyData()
     console.log "Toolbox for the user study is ready"
+
+  initLastDump : () ->
+    lastDump = localStorage.getItem 'lastDump'
+    if not lastDump?
+      localStorage.setItem 'lastDump', 0

   onMessage : (message, sender, sendResponse) ->
     switch(message.title)
@@ -14,9 +22,6 @@
         when "retrieveLabels" then @retrieveLabels sendResponse
         when "saveSession" then @saveSession message.content, sendResponse
         when "newActivity" then @checkIfNeedNewContext sendResponse
-        when "getUserStudyWebsites" then @getUserStudyWebsites sendResponse
-        when "storeUserStudyWebsite" then @storeUserStudyWebsite message.content, sendResponse
-        when "deleteUserStudyWebsite" then @deleteUserStudyWebsite message.content, sendResponse
         when "countDumpData" then @countDumpData sendResponse
         when "countDumpedData" then @countDumpedData sendResponse
         when "dumpData" then @dumpData sendResponse
@@ -63,84 +68,75 @@
     if (new Date().getTime() - @getLastActivityTime())>10*1000*60
       chrome.tabs.create {'url': chrome.extension.getURL('html/sessionInfo.html'), pinned:true}
     @updateLastActivityTime()
-
-  getUserStudyWebsites : (sendResponse) ->
-    query = new mem0r1es.Query().from("parameters").where("parameterId", "equals", "userStudyWebsites")
-    @storageManager.get query, (results) ->
-      if results.length is 1
-        sendResponse results[0].value
-        return
-      else
-        sendResponse {}
-        return
-      return
-    return
-
-  deleteUserStudyWebsite : (websiteId, sendResponse) =>
-    @getUserStudyWebsites (websites) =>
-      delete websites[websiteId]
-      @storageManager.store "parameters", {parameterId:"userStudyWebsites", value:websites}, sendResponse
-
-  storeUserStudyWebsite : (website, sendResponse) =>
-    @getUserStudyWebsites (websites) =>
-      websites[website.websiteId] = website
-      @storageManager.store "parameters", {parameterId:"userStudyWebsites", value:websites}, sendResponse

   countDumpedData : (sendResponse) ->
     sendResponse @currentCount

   countDumpData : (sendResponse) ->
-    @currentCount = 0
-    @getUserStudyWebsites (websites) =>
-      count = Object.keys(websites).length
-      if count is 0
-        sendResponse 1
+    query = new mem0r1es.Query().from("temporary")
+    @storageManager.count query, (results) =>
+      sendResponse results
+
+  dumpDailyData : () =>
+    lastDump = parseInt(localStorage.getItem('lastDump'), 10)
+    @now = new Date().getTime()
+    if @now < lastDump + 1000*60*60*24
+      console.log "no dump needed"
+      return
+    else
+      console.log "dumping the latest data"
+    query = new mem0r1es.Query().from("temporary").where("timestamp", "between", lastDump, true , @now, false).getChildren [{name:"userAction", objectStore:"userActions"},{name:"screenshot", objectStore:"screenshots"}]
+    @storageManager.get query, (results) =>
+      console.log "dumping #{results.length} pages"
+      page = results.shift()
+      @tidyDumpUp page, results, {}, @sendToServer
+    return
+
+  tidyDumpUp : (page, results, dump, callback) =>
+    if results.length is 0
+      callback dump
+      return
+
+    if dump[page._userStudySessionId]?
+      dump[page._userStudySessionId].pages.push page
+      page = results.shift()
+      @currentCount++
+      @tidyDumpUp page, results, dump, callback
+    else
+      query = new mem0r1es.Query().from("userStudySessions").where("userStudySessionId", "equals", parseInt(page._userStudySessionId, 10))
+      @storageManager.get query, (subResults) =>
+        dump[page._userStudySessionId] = {}
+        dump[page._userStudySessionId].userStudySession = subResults[0]
+        dump[page._userStudySessionId].pages = []
+        dump[page._userStudySessionId].pages.push page
+        page = results.shift()
+        @currentCount++
+        @tidyDumpUp page, results, dump, callback
+
+  sendToServer : (dump) =>
+    formData = new FormData()
+    formData.append "dump", JSON.stringify(dump)
+
+    xmlhttp = new XMLHttpRequest()
+    xmlhttp.open "POST", @dumpServerURL, true
+    xmlhttp.send(formData)
+    xmlhttp.onreadystatechange = () =>
+      if (xmlhttp.readyState isnt 4)
         return
-      total = 0
-      for websiteId, website of websites
-        @currentCount++
-        do (website) =>
-          query = new mem0r1es.Query().from("temporary").where("URL", "between", "#{website.pattern}", false , "#{website.pattern.slice 0,-1}#{String.fromCharCode(website.pattern.charCodeAt(website.pattern.length-1)+1) }", true)
-          @storageManager.count query, (results) =>
-            total += results
-            if count is 1
-              sendResponse total
-            else
-              count--
-
-  dumpData : (sendResponse) ->
+      if (xmlhttp.status is 200)
+        console.log "dump done."
+        localStorage.setItem 'lastDump', @now
+
+  dumpData : (sendResponse) =>
     console.log "dumping data for user study"
-    dump = {}
-    lastDump = localStorage.getItem 'lastActivityTime'
-    if lastDump is null
-      lastDump = 0
-
-    @getUserStudyWebsites (websites) =>
-      count = Object.keys(websites).length
-      if count is 0
-        @currentCount = 1
-        sendResponse dump
-        return
-      for websiteId, website of websites
-        do (website) =>
-          query = new mem0r1es.Query().from("temporary").where("URL", "between", "#{website.pattern}", false , "#{website.pattern.slice 0,-1}#{String.fromCharCode(website.pattern.charCodeAt(website.pattern.length-1)+1) }", true).getChildren [{name:"userAction", objectStore:"userActions"},{name:"screenshot", objectStore:"screenshots"}]
-
-          @storageManager.get query, (results) =>
-            subcount = results.length
-            for result in results
-              do(result) =>
-                query = new mem0r1es.Query().from("userStudySessions").where("userStudySessionId", "equals", parseInt(result._userStudySessionId, 10))
-                @storageManager.get query, (subResults) =>
-                  result.userStudySession = subResults[0]
-                  if subcount is 1
-                    dump[website.title] = results
-                    if count is 1
-                      sendResponse dump
-                    else
-                      count--
-                  else
-                    subcount--
-                    @currentCount++
+    @currentCount=0
+    query = new mem0r1es.Query().from("temporary").getChildren [{name:"userAction", objectStore:"userActions"},{name:"screenshot", objectStore:"screenshots"}]
+    @storageManager.get query, (results) =>
+      console.log "dumping #{results.length} pages"
+      page = results.shift()
+      @currentCount++
+      @tidyDumpUp page, results, {}, sendResponse
+    return

   storeMem0r1esFile : (messageContent, sendResponse) ->
     @storageManager.clearStore "temporary"
@@ -149,22 +145,19 @@
     @storageManager.clearStore "screenshots"
     try
       mem0ries = JSON.parse messageContent
-    catch error
-      response = "Invalid json"
-      sendResponse response
+    catch error
+      sendResponse "Invalid json"
+
+    count=0

-    count = 0
-    for key, websites of mem0ries
-      count = count + websites.length
-    for key, websites of mem0ries
-      for website in websites
-        @storageManager.store "userStudySessions", website.userStudySession
-        delete website.userStudySession
-        @storageManager.store "temporary", website, () =>
-          if count is 1
+    for key, session of mem0ries
+      @storageManager.store "userStudySessions", session.userStudySession
+      for page in session.pages
+        count++
+        @storageManager.store "temporary", page, () =>
+          count--
+          if count is 0
             sendResponse "Mem0r1es Loaded"
-          else
-            count--

   countMem0r1es : (sendResponse) =>
     query = new mem0r1es.Query().from("temporary")


Fix overwritting .run() in the data explorer

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -3435,9 +3435,8 @@
                 that = @
                 TermBase.prototype.private_run = TermBase.prototype.run
                 TermBase.prototype.run = ->
-                    if that.container.query_error?
-                        throw that.container.query_error_template
-                            found_run: true
+                    throw that.container.query_error_template
+                        found_run: true

         connect: =>
             that = @


Add a temporary fix for host+port

Add a temporary fix for host+port

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -527,7 +527,13 @@

         initialize: =>
             log_initial '(initializing) dataexplorer view:'
-            window.conn = new rethinkdb.net.HttpConnection 'http://newton:35457/', ''
+
+            host = window.location.hostname
+            if port is ''
+                port = 13457
+            else
+                port = parseInt(window.location.port)-1000+13457
+            window.conn = new rethinkdb.net.HttpConnection 'http://'+host+':'+port , ''
             window.r = rethinkdb.query


Suggestions are not broken when dot, parenthesis are used in the query

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -484,20 +484,35 @@

             return false

+        # Extract the last function of the current line
         extract_last_function: (query) =>
             start = 0
             count_dot = 0
             num_not_open_parenthesis = 0
+
+            is_string = false
+            char_used = ""
             for i in [query.length-1..0] by -1
-                if query[i] is ')'
-                    num_not_open_parenthesis++
-                else if query[i] is '('
-                    num_not_open_parenthesis--
-                else if query[i] is '.' and num_not_open_parenthesis <= 0
-                    count_dot++
-                    if count_dot is 2
-                        start = i+1
-                        break
+                if is_string is false
+                    if (query[i] is '"' or query[i] is '\'')
+                        is_string = true
+                        char_used = query[i]
+                    else if query[i] is '('
+                        num_not_open_parenthesis--
+                    else if query[i] is ')'
+                        num_not_open_parenthesis++
+                    else if query[i] is '.' and num_not_open_parenthesis <= 0
+                        count_dot++
+                        if count_dot is 2
+                            start = i+1
+                            break
+                else if is_string is true
+                    if query[i] is char_used
+                        if query[i-1]? and query[i-1] is '\\'
+                            continue
+                        else
+                            is_string = false
+
             dot_position = query.indexOf('.', start)
             dot_position = query.length if dot_position is -1
             parenthesis_position = query.indexOf('(', start)


Handle a 'null' response from the driver

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -1571,7 +1571,7 @@
                             @saved_data.query = @query
                             @saved_data.results = @current_results
                             @saved_data.metadata =
-                                limit_value: @current_results.length
+                                limit_value: if @current_results?.length? then @current_results.length else 1 # If @current_results.length is not defined, we have a single value
                                 skip_value: @skip_value
                                 execution_time: new Date() - @start_time
                                 query: @query
@@ -1612,7 +1612,7 @@
                     @saved_data.query = @query
                     @saved_data.results = @current_results
                     @saved_data.metadata =
-                        limit_value: @current_results.length
+                        limit_value: if @current_results?.length? then @current_results.length else 1 # If @current_results.length is not defined, we have a single value
                         skip_value: @skip_value
                         execution_time: new Date() - @start_time
                         query: @query
@@ -2191,7 +2191,7 @@
         default_size_column: 310 # max-width value of a cell of a table (as defined in the css file)

         render_result: (args) =>
-            if args?.results?
+            if args? and args.results isnt undefined
                 @results = args.results
                 @results_array = null # if @results is not an array (possible starting from 1.4), we will transform @results_array to [@results] for the table view
             if args?.metadata?
@@ -2210,13 +2210,20 @@
                         minutes = Math.floor(args.metadata.execution_time/(60*1000))
                         @metadata.execution_time_pretty = minutes+"min "+((args.metadata.execution_time-minutes*60*1000)/1000).toFixed(2)+"s"

+            num_results = @metadata.skip_value
+            if @metadata.has_more_data isnt true
+                if @results?.length?
+                    num_results += @results.length
+                else # @results can be a single value or null
+                    num_results += 1
+
             @.$el.html @template _.extend @metadata,
                 show_query_warning: args?.show_query_warning
                 show_more_data: @metadata.has_more_data is true and @container.saved_data.cursor_timed_out is false
                 cursor_timed_out_template: (@cursor_timed_out_template() if @metadata.has_more_data is true and @container.saved_data.cursor_timed_out is true)
                 execution_time_pretty: @metadata.execution_time_pretty
-                no_results: @metadata.has_more_data isnt true and @results.length is 0 and @metadata.skip_value is 0
-                num_results: ((@metadata.skip_value+@results.length) if @metadata.has_more_data isnt true)
+                no_results: @metadata.has_more_data isnt true and @results?.length is 0 and @metadata.skip_value is 0
+                num_results: num_results

             switch @view
                 when 'tree'


pause feed when mousedown on results

pause feed when mousedown on results

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -2875,6 +2875,7 @@
         events: ->
             'click .jt_arrow': 'toggle_collapse'
             'click .jta_arrow_h': 'expand_tree_in_table'
+            'mousedown': 'parent_pause_feed'

         initialize: (args) =>
             @parent = args.parent
@@ -2948,6 +2949,9 @@
             $('.'+classname_to_change).css 'max-width', 'none'
             dom_element.css 'max-width', 'none'
             @parent.set_scrollbar()
+
+        parent_pause_feed: (event) =>
+            @parent.pause_feed()

         pause_feed: =>
             @parent.container.state.pause_at = @query_result.size()


Display dates as  strings

Display dates as  strings

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -2802,6 +2802,10 @@
                     sub_values[sub_values.length-1]['no_comma'] = true
                     return @template_json_tree.array
                         values: sub_values
+            else if value_type is 'object' and value.$reql_type$ is 'TIME' and value.epoch_time?
+                return @template_json_tree.span
+                    classname: 'jt_date'
+                    value: new Date(value.epoch_time)
             else if value_type is 'object'
                 sub_keys = []
                 for key of value
@@ -2861,15 +2865,18 @@
             result = args.result

             if jQuery.isPlainObject(result)
-                for key, row of result
-                    if not keys_count['object']?
-                        keys_count['object'] = {} # That's define only if there are keys!
-                    if not keys_count['object'][key]?
-                        keys_count['object'][key] =
-                            primitive_value_count: 0
-                    @build_map_keys
-                        keys_count: keys_count['object'][key]
-                        result: row
+                if result.$reql_type$ is 'TIME' and result.epoch_time?
+                    keys_count.primitive_value_count++
+                else
+                    for key, row of result
+                        if not keys_count['object']?
+                            keys_count['object'] = {} # That's define only if there are keys!
+                        if not keys_count['object'][key]?
+                            keys_count['object'][key] =
+                                primitive_value_count: 0
+                        @build_map_keys
+                            keys_count: keys_count['object'][key]
+                            result: row
             else
                 keys_count.primitive_value_count++

@@ -3036,6 +3043,9 @@
                 else
                     data['value'] = '[ ... ]'
                     data['data_to_expand'] = JSON.stringify(value)
+            else if value_type is 'object' and value.$reql_type$ is 'TIME' and value.epoch_time?
+                data['value'] = (new Date(value.epoch_time)).toString()
+                data['classname'] = 'jta_date'
             else if value_type is 'object'
                 data['value'] = '{ ... }'
                 data['is_object'] = true


zoning in specs

zoning in specs

--- test/components/task-plan/builder/index.spec.coffee
+++ test/components/task-plan/builder/index.spec.coffee
@@ -22,8 +22,8 @@
 tomorrow = moment(TimeStore.getNow()).add(1, 'day').format(ISO_DATE_FORMAT)
 dayAfter = moment(TimeStore.getNow()).add(2, 'day').format(ISO_DATE_FORMAT)

-getDateString = (value) -> TimeHelper.getMomentPreserveDate(value).format(TutorDateFormat)
-getISODateString = (value) -> TimeHelper.getMomentPreserveDate(value).format(ISO_DATE_FORMAT)
+getDateString = (value) -> TimeHelper.getZonedMoment(value).format(TutorDateFormat)
+getISODateString = (value) -> TimeHelper.getZonedMoment(value).format(ISO_DATE_FORMAT)

 COURSES = require '../../../../api/user/courses.json'
 NEW_READING = ExtendBasePlan({id: "_CREATING_1", settings: {page_ids: []}}, false, false)


Fix typo in settings `templates`->`template`

--- server-ce/settings.coffee
+++ server-ce/settings.coffee
@@ -283,7 +283,7 @@
 			ignoreTLS: parse(process.env["SHARELATEX_EMAIL_SMTP_IGNORE_TLS"])

 		textEncoding:  process.env["SHARELATEX_EMAIL_TEXT_ENCODING"]
-		templates:
+		template:
 			customFooter: process.env["SHARELATEX_CUSTOM_EMAIL_FOOTER"]

 	if process.env["SHARELATEX_EMAIL_SMTP_USER"]? or process.env["SHARELATEX_EMAIL_SMTP_PASS"]?


:bug:Fix license request email parsing

:bug:Fix license request email parsing

Caused by
https://github.com/codecombat/codecombat/commit/a6bb706cf295e2680fe77ff1
3e91d3da59487b53

--- server/lib/closeIO.coffee
+++ server/lib/closeIO.coffee
@@ -65,7 +65,7 @@
           activities = JSON.parse(body)
           return done("Unexpected activities format: " + body) unless activities.data?
           for activity in activities.data when activity._type is 'Email'
-            if /@codecombat\.(?:com)|(?:nl)$/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0
+            if /@codecombat\.(?:com)|(?:nl)/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0
               return done(null, activity.sender, lead.id)
           return done(null, config.mail.supportSchools, lead.id)
     catch error


popup: cleanup

popup: cleanup

--- cs/popup.coffee
+++ cs/popup.coffee
@@ -134,12 +134,8 @@
   feed = 'nabla'
   News.get feed, newsLimit, (items) ->

-    # Find most recent post, return if we've already seen it
+    # Find most recent post and save it
     mostRecent = items[0].link
-    # if ls.mostRecentRead is mostRecent
-    #   if $('#news').text().trim() isnt '' # News box empty already
-    #     if DEBUG then console.log 'No new news'
-    #     return
     ls.mostRecentRead = mostRecent
     $('#news').html ''


remove leftover sortable activation call

--- jsapp/xlform/src/view.surveyApp.coffee
+++ jsapp/xlform/src/view.surveyApp.coffee
@@ -646,8 +646,6 @@
       null_top_row = @formEditorEl.find(".survey-editor__null-top-row").removeClass("expanded")
       null_top_row.toggleClass("survey-editor__null-top-row--hidden", !isEmpty)

-      @activateSortable() # TODO remove dev temp
-
       if (
         @features.multipleQuestions and
         not (


sankey: use gradient on loss link

sankey: use gradient on loss link

--- app/assets/javascripts/d3/sankey.coffee
+++ app/assets/javascripts/d3/sankey.coffee
@@ -363,6 +363,16 @@
       @links = @draw_links()
       @nodes = @draw_nodes()

+      # Let's create a gradient
+      defs = @svg.append('svg:defs')
+      defs.append('svg:linearGradient')
+        .attr('x1', "0%").attr('y1', "0%").attr('x2', "100%").attr('y2', "0%")
+        .attr('id', 'loss_gradient').call(
+          (g) ->
+            g.append('svg:stop').attr('offset', '0%').attr('style', 'stop-color:rgb(250,0,0);stop-opacity:1')
+            g.append('svg:stop').attr('offset', '100%').attr('style', 'stop-color:rgb(250,0,0);stop-opacity:0.1')
+      )
+
     draw_links: =>
       # links are treated as a group
       links = @svg.selectAll('g.link')
@@ -373,7 +383,9 @@
       # link path
       links.append("svg:path")
         .style("stroke-width", (link) -> link.value())
-        .style("stroke", (link, i) -> link.color())
+        .style("stroke", (link) ->
+          if link.connects('loss') then "url('#loss_gradient')" else link.color()
+        )
         .style("fill", "none")
         .style("opacity", selectedLinkOpacity)
         .attr("d", (link) => @link_line link.path_points())


Added scroll to map

Added scroll to map

--- app/assets/javascripts/fbombs.js.coffee
+++ app/assets/javascripts/fbombs.js.coffee
@@ -9,7 +9,8 @@
     mapTypeId: google.maps.MapTypeId.ROADMAP,
     disableDefaultUI: true,
     disableDoubleClickZoom: true,
-    scrollwheel: false
+    zoomControl: true
+    # scrollwheel: false
   map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions)

 $.get "tweets", (data) ->


Put best_in_place Back Into global.js

Put best_in_place Back Into global.js

Re-insert the best_in_place function calls into the global JS since they
are required for checkbox (Free/Paid) capabilities.

--- app/assets/javascripts/global.js.coffee
+++ app/assets/javascripts/global.js.coffee
@@ -10,6 +10,11 @@
     $(".modal .modal-body").html   ""
     $(".modal .modal-footer").html "Close"
     $(".modal").modal "show"
+
+jQuery ->
+  # set up in-line editing
+  $(".best_in_place").each ->
+    $(this).best_in_place()

 # mercury update hooks
 jQuery(window).on "mercury:ready", ->


Removed unused js requires

Removed unused js requires

--- app/assets/javascripts/places.js.coffee
+++ app/assets/javascripts/places.js.coffee
@@ -2,10 +2,6 @@
 # All this logic will automatically be available in application.js.
 # You can use CoffeeScript in this file: http://coffeescript.org/

-#= require riot/riot.min
-#= require components/items-section.js
-#= require components/item.js
-#= require components/sidebar-left.js
 #= require gmap3
 #= require gmaps-builder-user
-#= require bLazy/blazy+#= require bLazy/blazy


Fix a bug when reediting the same paragraph/link

--- app/assets/javascripts/redaction.coffee
+++ app/assets/javascripts/redaction.coffee
@@ -31,10 +31,15 @@
       $(".locked").removeClass "locked"
       $.noticeAdd text: "Tous les verrous ont été supprimés"

+    innerHtmlForLink: (msg) ->
+      """
+      #{msg.title} (0 clic)
+      """
+
     htmlForLink: (msg) ->
       """

-        #{msg.title} (0 clic)
+        #{innerHtmlForLink msg}

       """

@@ -43,8 +48,9 @@
       $("#link_#{msg.id}").editionInPlace()

     onUpdateLink: (msg) =>
-      $("#link_#{msg.id}").replaceWith @htmlForLink(msg)
-      $("#link_#{msg.id}").removeClass "locked"
+      $("#link_#{msg.id}").html(@innerHtmlForLink msg)
+                          .attr(lang: msg.lang)
+                          .removeClass "locked"

     onRemoveLink: (msg) ->
       $("#link_#{msg.id}").remove()
@@ -64,8 +70,8 @@
       $("#paragraph_#{msg.id}").editionInPlace()

     onUpdateParagraph: (msg) =>
-      $("#paragraph_#{msg.id}").replaceWith @htmlForPara(msg)
-      $("#paragraph_#{msg.id}").removeClass "locked"
+      $("#paragraph_#{msg.id}").html(msg.body)
+                               .removeClass "locked"

     onRemoveParagraph: (msg) ->
       $("#paragraph_#{msg.id}").remove()


Change js filename to gss-parser.

Change js filename to gss-parser.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -13,7 +13,7 @@
     componentbuild:
       'ccss-compiler':
         options:
-          name: 'ccss-compiler'
+          name: 'gss-parser'
         src: '.'
         dest: 'browser'
         scripts: true
@@ -25,7 +25,7 @@
         report: 'min'
       ccssCompiler:
         files:
-          './browser/ccss-compiler.min.js': ['./browser/ccss-compiler.js']
+          './browser/gss-parser.min.js': ['./browser/gss-parser.js']

     # Automated recompilation and testing when developing
     watch:


Dry configuration.

Dry configuration.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -209,7 +209,6 @@

     jasminehtml:
       options:
-        karmaConfigFile: "<%= appConfig.test %>/karma.conf.coffee"
         dest: "<%= appConfig.dev %>"

     casperjs:
@@ -321,7 +320,9 @@

   grunt.registerTask "jasminehtml", ->
     path = require("path")
+
     options = @options()
+    options.karmaConfigFile or= grunt.config("karma.unit.configFile")

     loadKarmaPatterns = ->
       files = []


Fixed indentation for issue #1

Fixed indentation for issue #1


Apparently we don’t need to reload. [#89552722]

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -138,11 +138,6 @@
           cwd: currentDir
         files: 'package.json'
         tasks: 'copy:package'
-      reloadAfterDelete:
-        files: '**/*'
-        event: ['deleted']
-        options:
-          reload: true
       sass:
         files: sassFiles
         options:


Update license banner

Update license banner

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,6 +1,7 @@
 # grunt-git-committers
 # https://github.com/dciccale/grunt-git-committers
-# Copyright (c) 2013 Denis Ciccale (@tdecs)
+#
+# Copyright (c) 2013-2014 Denis Ciccale (@tdecs)
 # Licensed under the MIT license.
 # https://github.com/dciccale/grunt-git-committers/blob/master/LICENSE-MIT


Run connect before PhantomJS

Run connect before PhantomJS

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -131,11 +131,9 @@

     # BDD tests on browser
     mocha_phantomjs:
-      options:
-        output: 'spec/result.xml'
-        reporter: 'spec'
       all:
         options:
+          reporter: 'spec'
           urls: ['http://127.0.0.1:9999/spec/runner.html']

     'saucelabs-mocha':
@@ -182,7 +180,7 @@

   @registerTask 'build-fast', ['coffee', 'concat:worker', 'exec:component_build']
   @registerTask 'build', ['coffee', 'concat:worker', 'uglify:worker', 'exec', 'uglify:engine', 'usebanner']
-  @registerTask 'test', ['build', 'coffeelint', 'mocha_phantomjs']
+  @registerTask 'test', ['build', 'coffeelint', 'connect', 'mocha_phantomjs']
   @registerTask 'crossbrowser', ['build', 'coffeelint', 'connect', 'mocha_phantomjs', 'saucelabs-mocha']
   @registerTask 'default', ['build']
   @registerTask 'nuke', ['clean']


Gruntfile: watch less changes too

Gruntfile: watch less changes too

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -142,7 +142,7 @@

     # task to watch sources for changes and recompile during development
     watch:
-      files: paths.coffee_src
+      files: [paths.coffee_src, paths.less_src]
       tasks: 'deps' # or 'test', or 'testserver' :)

     # task to run shell commands


Allowing tests to run on port as specified by --port

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,4 +1,7 @@
 module.exports = (grunt) ->
+  serverPort = grunt.option('port') || 8890
+  inlinePort = grunt.option('port') || 8891
+
   path = require 'path'

   grunt.loadNpmTasks 'grunt-contrib-coffee'
@@ -29,10 +32,10 @@

   grunt.registerTask 'test:__buildjasmineconf__', 'Internal task to build and alter the jasmine conf', ['jasmine:apps:build', 'replace:jasmine']
   grunt.registerTask 'test:conf', 'Fetches the deps, compiles coffee and css files, runs jshint and builds the jasmine test config', ['nexus:deps', 'clean:test', 'coffee', 'css', 'test:__buildjasmineconf__']
-  grunt.registerTask 'test', 'Sets up and runs the tests in the default browser. Use --browser=<other> to run in a different browser.', ['test:conf', 'express:inline', 'webdriver_jasmine_runner:apps']
+  grunt.registerTask 'test', 'Sets up and runs the tests in the default browser. Use --browser=<other> to run in a different browser, and --port=<port> for a different port.', ['test:conf', 'express:inline', 'webdriver_jasmine_runner:apps']
   grunt.registerTask 'test:chrome', 'Sets up and runs the tests in Chrome', ['test:conf', 'express:inline', 'webdriver_jasmine_runner:chrome']
   grunt.registerTask 'test:firefox', 'Sets up and runs the tests in Firefox', ['test:conf', 'express:inline', 'webdriver_jasmine_runner:firefox']
-  grunt.registerTask 'test:server', 'Starts a Jasmine server at localhost:8890', ['express:server', 'express-keepalive']
+  grunt.registerTask 'test:server', "Starts a Jasmine server at localhost:#{serverPort}, specify a different port with --port=<port>", ['express:server', 'express-keepalive']
   grunt.registerTask 'test:ci', 'Runs the tests in both firefox and chrome', ['test:conf', 'express:inline', 'webdriver_jasmine_runner:chrome', 'webdriver_jasmine_runner:firefox']

   _ = grunt.util._
@@ -104,15 +107,15 @@
         debug: debug
       server:
         options:
-          port: grunt.option('port') || 8890
+          port: serverPort
       inline:
         options:
-          port: grunt.option('port') || 8891
+          port: inlinePort

     webdriver_jasmine_runner:
       options:
         seleniumServerArgs: ['-Xmx256M']
-        testServerPort: 8891
+        testServerPort: inlinePort
       apps: {}
       chrome:
         options:
@@ -184,7 +187,7 @@
             "#{appsdk_path}/builds/rui/resources/css/lib-closure.css"
             'build/resources/css/catalog-all.css'
           ]
-          host: 'http://127.0.0.1:8891/'
+          host: "http://127.0.0.1:#{inlinePort}/"

     replace:
       jasmine:


Switched from `nyan` to `dot`.

Switched from `nyan` to `dot`.

It turns out that using `nyan` as the mocha reporter interacts badly with console printing, which I still use to debug occasionally.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -47,7 +47,7 @@
           timeout: 3000
           ignoreLeaks: false
           ui: 'bdd'
-          reporter: 'nyan' # 'progress' # 'min' # 'dot' # 'spec'
+          reporter: 'dot' # 'progress' # 'min' # 'dot' # 'spec'
     watch:
       options:
         spawn: false


finally gotcha, you son of a ....

finally gotcha, you son of a ....

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -18,6 +18,7 @@
         src: ['lib/doofinder.js']
         dest: 'dist/doofinder.js'
         options:
+          exclude: ['jsdom']
           browserifyOptions:
             standalone: 'doofinder'


build distribution in `dist/` folder

build distribution in `dist/` folder

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -25,7 +25,7 @@

             build:
                 src: ['monapt.ts']
-                dest: 'monapt.js'
+                dest: 'dist/monapt.js'
                 options:
                     module: 'commonjs'
                     target: 'es3'
@@ -35,12 +35,12 @@
             type:
                 src: ['compiled/**/*.js', 'compiled/**/*']
             build:
-                src: ['monapt.js', 'monapt.min.js', 'monapt.d.ts']
+                src: ['dist/monapt.js', 'dist/monapt.min.js', 'dist/monapt.d.ts']

         uglify:
             min:
                 files:
-                    'monapt.min.js': ['monapt.js']
+                    'dist/monapt.min.js': ['dist/monapt.js']
             ###
             options:
                 mangle:


Change copy:css task name to copy:cssmap.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -70,11 +70,14 @@
         src: ['javascripts/*.js']
         dest: '_public/'

+      # Note: this task is only used to copy CSS map files. The
+      # CSS style files themselves are copied to the destination
+      # in the concat:css task.
+      #
+      # This task is only used to copy the Bootstrap map file.
       # Since the non-minimized Bootstrap module references the
       # CSS map, the map must be colocated with the stylesheets.
-      # The CSS style files themselves are copied to the destination
-      # in the concat:css task.
-      css:
+      cssmap:
         expand: true
         flatten: true
         cwd: 'bower_components/'
@@ -276,7 +279,7 @@
   grunt.registerTask 'copy:js', ['copy:bower', 'copy:appjs']

   # Assemble the app.
-  grunt.registerTask 'copy:app', ['copy:js', 'copy:css', 'copy:fonts',
+  grunt.registerTask 'copy:app', ['copy:js', 'copy:cssmap', 'copy:fonts',
                                   'copy:static']

   # Concatenate the app stylesheets.


Attempt to fix appcache error for bower_components/requirejs

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -58,7 +58,7 @@
         dest: 'browser/manifest.appcache'
         src: [
           'everything.*'
-          'bower_components/*'
+          'bower_components/*/*'
         ]

     'string-replace':


Separate noflo and polymer builds in Grunt

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -255,10 +255,16 @@
   @registerTask 'nuke', [
     'clean'
   ]
-  @registerTask 'build', [
+  @registerTask 'build_noflo', [
     'webpack'
+  ]
+  @registerTask 'build_polymer', [
     'sharedstylecomponent'
     'exec:vulcanize'
+  ]
+  @registerTask 'build', [
+    'build_noflo'
+    'build_polymer'
     'string-replace:app'
     'compress'
   ]


Run npm test from grunt test

Run npm test from grunt test

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -12,7 +12,7 @@

     shell:
       test:
-        command: 'jasmine-node --coffee --captureExceptions spec/'
+        command: 'npm test'
         options:
           stdout: true
           stderr: true


Don’t build the docs when running `grunt test`

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -204,8 +204,10 @@

   grunt.registerTask "default", ["run"]
   grunt.registerTask "run", ["build", "connect", "open", "watch"]
-  grunt.registerTask "build", ["clean", "coffeelint", "coffee", "less", "concat", "uglify", "copy", "jekyll"]
-  grunt.registerTask "test", ["build", "jasmine"]
+  grunt.registerTask "build-code", ["clean", "coffeelint", "coffee", "less", "concat", "uglify", "copy"]
+  grunt.registerTask "build-docs", ["jekyll"]
+  grunt.registerTask "build", ["build-code", "build-docs"]
+  grunt.registerTask "test", ["build-code", "jasmine"]
   grunt.registerTask "release", "Release a new version, push it and publish it", (target) ->
     target = "patch" unless target
     grunt.task.run "bump-only:#{target}", "test", "replace", "bump-commit"


Remove redundant checks (#424)

Remove redundant checks (#424)

--- app/core/store/modules/game.ozar.coffee
+++ app/core/store/modules/game.ozar.coffee
@@ -73,13 +73,8 @@
     addTutorialStepsFromSayEvents: ({ commit, rootState }, sayEvents) ->
       sayEvents.forEach((sayEvent) ->
         { say, tutorial } = sayEvent
-        if say.i18n
-          text = utils.i18n(say, 'text')
-        if not text
-          text = say.text
-
         commit('addTutorialStep', {
-          message: text
+          message: utils.i18n(say, 'text')
           # To stay backwards compatible with old Vega messages,
           # they are turned into stationary Vega messages with no other qualities:
           position: tutorial?.position or 'stationary'


Compress modules

Compress modules

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -42,7 +42,7 @@
       spec        : files: '<%= meta.build %>/<%= meta.file %>.spec.js'       : '<%=meta.build%>/<%=meta.file%>.spec.coffee'

     uglify:
-      options: mangle: false, banner: "<%= meta.banner %>", report: "gzip"
+      options: mangle: true, banner: "<%= meta.banner %>", report: "gzip"
       core        : files: '<%= meta.bower %>/<%= meta.file %>.js'            : '<%= meta.build %>/<%= meta.file %>.js'
       ajax        : files: '<%= meta.bower %>/<%= meta.file %>.ajax.js'       : '<%= meta.build %>/<%= meta.file %>.ajax.js'
       css         : files: '<%= meta.bower %>/<%= meta.file %>.css.js'        : '<%= meta.build %>/<%= meta.file %>.css.js'


hmm grunt...

hmm grunt...

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -4,6 +4,10 @@
   #  Load npm tasks
   #-----------------------------------------------
   require('matchdep').filterDev('grunt-*').forEach grunt.loadNpmTasks
+
+  #  Option
+  #-----------------------------------------------
+  filter = grunt.option 'filter'

   #  File list
   #-----------------------------------------------
@@ -153,6 +157,9 @@

     # Watch
     watch:
+      options:
+        spawn: false
+
       coffee:
         files: 'src/**/*.coffee'
         tasks: ['coffee:src']
@@ -162,7 +169,7 @@
         tasks: ['coffee:test']

       jasmine:
-        files: 'tmp/spec/**/*.js'
+        files: ['tmp/spec/**/*.js', 'tmp/src/**/*.js']
         tasks: ['filtered_test']

@@ -171,7 +178,7 @@
   grunt.option 'force', true
   grunt.registerTask 'default', ['dev']

-  filteredTest = if (filter = grunt.option('filter')) then "jasmine:#{filter}" else 'jasmine'
+  filteredTest = if filter then "jasmine:#{filter}" else 'jasmine'
   grunt.registerTask 'filtered_test', [filteredTest]

   grunt.registerTask 'dev', ['coffee:src', 'watch:coffee']


Update node-webkit version to 0.9.2

Update node-webkit version to 0.9.2

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -3,7 +3,7 @@
     nodewebkit:
       options:
         build_dir: './builds'
-        version: '0.8.4'
+        version: '0.9.2'
         mac_icns: './src/assets/icon.icns'
         mac: true
         win: true


coffeelint: whitespace only

coffeelint: whitespace only

--- share/coffee/index.coffee
+++ share/coffee/index.coffee
@@ -1,522 +1,552 @@
 require.config
-    shim: {bootstrap: {deps: ['jquery']}}
-    baseUrl: '/js/devel'
-    paths:
-        domReady:  'vendor/min/requirejs-domReady/2.0.1/domReady'
-        headroom:  'vendor/min/headroom/0.7.0/headroom.min'
-        jquery:    'vendor/min/jquery/2.1.3/jquery-2.1.3.min'
-        bootstrap: 'vendor/min/bootstrap/3.3.2/bootstrap.min'
-        react:     'vendor/min/react/0.12.2/react.min'
-        jscript:   'gen/jscript'
-
-require ['jquery', 'bootstrap', 'react', 'jscript'],
-        ($,        _,           React, jscript) ->
-                @neweventsource = (onmessage) ->
-                        conn = null
-                        sendSearch = (search) ->
-                                # conn = new EventSource('/index.sse' + search)
-                                # location.search = search
-                                console.log('SEARCH', search)
-                                conn.close() # if conn?
-                                window.setTimeout(init, 1000)
-                        sendClient = (client) ->
-                                # TODO
-                                return
-                                console.log(JSON.stringify(client), 'sendClient')
-                                obj = {Client: client}
-                                # 0 conn.CONNECTING
-                                # 1 conn.OPEN
-                                # 2 conn.CLOSING
-                                # 3 conn.CLOSED
-                                if !conn? ||
-                                   conn.readyState == conn.CLOSING ||
-                                   conn.readyState == conn.CLOSED
-                                        init()
-                                if !conn? ||
-                                   conn.readyState != conn.OPEN
-                                        console.log('Not connected, cannot send', obj)
-                                        return
-                                return conn.send(JSON.stringify(obj))
-                        init = () ->
-                                conn = new EventSource('/index.sse' + location.search)
-                                conn.onopen = () ->
-                                        $(window).bind('popstate', (() ->
-                                                sendSearch(location.search)
-                                                return))
-                                        return
-
-                                statesel = 'table thead tr .header a.state'
-                                again = (e) ->
-                                        $(statesel).unbind('click')
-                                        window.setTimeout(init, 5000) if !e.wasClean
-                                        return
-
-                                conn.onclose   = () -> console.log('sse closed (should recover)')
-                                conn.onerror   = () -> console.log('sse errord (should recover)')
-                                conn.onmessage = onmessage
-
-                                $(statesel).click(() ->
-                                        history.pushState({path: @path}, '', @href)
-                                        sendSearch(@search)
-                                        return false)
-                                return
-
-                        init()
-                        return {
-                                sendClient: sendClient
-                                sendSearch: sendSearch
-                                close: () -> conn.close()
-                        }
-                @newwebsocket = (onmessage) ->
-                        conn = null
-                        sendSearch = (search) -> sendJSON({Search: search})
-                        sendClient = (client) ->
-                                console.log(JSON.stringify(client), 'sendClient')
-                                return sendJSON({Client: client})
-                        sendJSON = (obj) ->
-                                # 0 conn.CONNECTING
-                                # 1 conn.OPEN
-                                # 2 conn.CLOSING
-                                # 3 conn.CLOSED
-                                if !conn? ||
-                                   conn.readyState == conn.CLOSING ||
-                                   conn.readyState == conn.CLOSED
-                                        init()
-                                if !conn? ||
-                                   conn.readyState != conn.OPEN
-                                        console.log('Not connected, cannot send', obj)
-                                        return
-                                return conn.send(JSON.stringify(obj))
-                        init = () ->
-                                hostport = window.location.hostname + (if location.port then ':' + location.port else '')
-                                conn = new WebSocket('ws://' + hostport + '/index.ws')
-                                conn.onopen = () ->
-                                        sendSearch(location.search)
-                                        $(window).bind('popstate', (() ->
-                                                sendSearch(location.search)
-                                                return))
-                                        return
-
-                                statesel = 'table thead tr .header a.state'
-                                again = (e) ->
-                                        $(statesel).unbind('click')
-                                        window.setTimeout(init, 5000) if !e.wasClean
-                                        return
-
-                                conn.onclose   = again
-                                conn.onerror   = again
-                                conn.onmessage = onmessage
-
-                                $(statesel).click(() ->
-                                        history.pushState({path: @path}, '', @href)
-                                        sendSearch(@search)
-                                        return false)
-                                return
-
-                        init()
-                        return {
-                                sendClient: sendClient
-                                sendSearch: sendSearch
-                                close: () -> conn.close()
-                        }
-
-                @IFbytesCLASS = React.createClass
-                        getInitialState: () -> Data.IFbytes # a global Data
-                        render: () ->
-                                Data = {IFbytes: @state}
-                                return jscript.ifbytes_table(Data, (jscript.ifbytes_rows(Data, $if) for $if in Data?.IFbytes?.List ? []))
-
-                @IFerrorsCLASS = React.createClass
-                        getInitialState: () -> Data.IFerrors # a global Data
-                        render: () ->
-                                Data = {IFerrors: @state}
-                                return jscript.iferrors_table(Data, (jscript.iferrors_rows(Data, $if) for $if in Data?.IFerrors?.List ? []))
-
-                @IFpacketsCLASS = React.createClass
-                        getInitialState: () -> Data.IFpackets # a global Data
-                        render: () ->
-                                Data = {IFpackets: @state}
-                                return jscript.ifpackets_table(Data, (jscript.ifpackets_rows(Data, $if) for $if in Data?.IFpackets?.List ? []))
-
-                @DFbytesCLASS = React.createClass
-                        getInitialState: () -> {DFlinks: Data.DFlinks, DFbytes: Data.DFbytes} # a global Data
-                        render: () ->
-                                Data = @state
-                                return jscript.dfbytes_table(Data, (jscript.dfbytes_rows(Data, $disk) for $disk in Data?.DFbytes?.List ? []))
-
-                @DFinodesCLASS = React.createClass
-                        getInitialState: () -> {DFlinks: Data.DFlinks, DFinodes: Data.DFinodes} # a global Data
-                        render: () ->
-                                Data = @state
-                                return jscript.dfinodes_table(Data, (jscript.dfinodes_rows(Data, $disk) for $disk in Data?.DFinodes?.List ? []))
-
-                @MEMtableCLASS = React.createClass
-                        getInitialState: () -> Data.MEM # a global Data
-                        render: () ->
-                                Data = {MEM: @state}
-                                return jscript.mem_table(Data, (jscript.mem_rows(Data, $mem) for $mem in Data?.MEM?.List ? []))
-
-                @CPUtableCLASS = React.createClass
-                        getInitialState: () -> Data.CPU # a global Data
-                        render: () ->
-                                Data = {CPU: @state}
-                                return jscript.cpu_table(Data, (jscript.cpu_rows(Data, $core) for $core in Data?.CPU?.List ? []))
-
-                @PStableCLASS = React.createClass
-                        getInitialState: () -> {PStable: Data.PStable, PSlinks: Data.PSlinks} # a global Data
-                        render: () ->
-                                Data = @state
-                                return jscript.ps_table(Data, (jscript.ps_rows(Data, $proc) for $proc in Data?.PStable?.List ? []))
-
-                @VGtableCLASS = React.createClass
-                        getInitialState: () -> { # a global Data:
-                                VagrantMachines: Data.VagrantMachines
-                                VagrantError:    Data.VagrantError
-                                VagrantErrord:   Data.VagrantErrord
-                        }
-                        render: () ->
-                                Data = @state
-                                if Data?.VagrantErrord? and Data.VagrantErrord
-                                        rows = [jscript.vagrant_error(Data)]
-                                else
-                                        rows = (jscript.vagrant_rows(Data, $mach) for $mach in Data?.VagrantMachines?.List ? [])
-                                return jscript.vagrant_table(Data, rows)
-
-                @addNoscript = ($) -> $.append('<noscript />').find('noscript').get(0)
-
-                @HideClass = React.createClass
-                        statics: component: (opt) -> React.render(HideClass(opt), addNoscript(opt.$button_el))
-
-                        reduce: (data) ->
-                                if data?.Client?
-                                        value = data.Client[@props.xkey]
-                                        return {Hide: value} if value isnt undefined
-                                return null
-                        getInitialState: () -> @reduce(Data) # a global Data
-                        componentDidMount: () -> @props.$button_el.click(@click)
-                        render: () ->
-                                @props.$collapse_el.collapse(if @state.Hide then 'hide' else 'show')
-                                buttonactive =  @state.Hide
-                                buttonactive = !@state.Hide if @props.reverseActive? and @props.reverseActive
-                                @props.$button_el[if buttonactive then 'addClass' else 'removeClass']('active')
-                                return null
-                        click: (e) ->
-                                (S = {})[@props.xkey] = !@state.Hide
-                                updates.sendClient(S)
-                                e.stopPropagation() # preserves checkbox/radio
-                                e.preventDefault()  # checked/selected state
-                                return undefined
-
-                @ButtonClass = React.createClass
-                        statics: component: (opt) -> React.render(ButtonClass(opt), addNoscript(opt.$button_el))
-
-                        reduce: (data) ->
-                                if data?.Client?
-                                        S = {}
-                                        S.Hide = data.Client[@props.Khide] if                   data.Client[@props.Khide] isnt undefined # Khide is a required prop
-                                        S.Able = data.Client[@props.Kable] if @props.Kable? and data.Client[@props.Kable] isnt undefined
-                                        S.Send = data.Client[@props.Ksend] if @props.Ksend? and data.Client[@props.Ksend] isnt undefined
-                                        S.Text = data.Client[@props.Ktext] if @props.Ktext? and data.Client[@props.Ktext] isnt undefined
-                                        return S
-                        getInitialState: () -> @reduce(Data) # a global Data
-                        componentDidMount: () -> @props.$button_el.click(@click)
-                        render: () ->
-                                if @props.Kable
-                                        able = @state.Able
-                                        able = !able if not (@props.Kable.indexOf('not') > -1) # That's a hack
-                                        @props.$button_el.prop('disabled', able)
-                                        @props.$button_el[if able then 'addClass' else 'removeClass']('disabled')
-                                @props.$button_el[if @state.Send then 'addClass' else 'removeClass']('active') if @props.Ksend?
-                                @props.$button_el.text(@state.Text) if @props.Ktext?
-                                return null
-                        click: (e) ->
-                                S = {}
-                                S[@props.Khide] = !@state.Hide if @state.Hide?  and @state.Hide # if the panel was hidden
-                                S[@props.Ksend] = !@state.Send if @props.Ksend? and @state.Send? # Q is a @state.Send? check excessive?
-                                S[@props.Ksig]  =  @props.Vsig if @props.Ksig?
-                                updates.sendClient(S)
-                                e.stopPropagation() # preserves checkbox/radio
-                                e.preventDefault()  # checked/selected state
-                                return undefined
-
-                @TabsClass = React.createClass
-                        statics: component: (opt) -> React.render(TabsClass(opt), addNoscript(opt.$button_el))
-
-                        reduce: (data) ->
-                                if data?.Client?
-                                        S = {}
-                                        S.Hide = data.Client[@props.Khide] if                   data.Client[@props.Khide] isnt undefined # Khide is a required prop
-                                        S.Send = data.Client[@props.Ksend] if @props.Ksend? and data.Client[@props.Ksend] isnt undefined
-                                        return S
-                        getInitialState: () -> @reduce(Data) # a global Data
-                        componentDidMount: () ->
-                                @props.$button_el.click(@clicktab)
-                                @props.$hidebutton_el.click(@clickhide)
-                        render: () ->
-                                if @state.Hide
-                                        @props.$collapse_el.collapse('hide')
-                                        @props.$hidebutton_el.addClass('active')
-                                        return null
-                                @props.$hidebutton_el.removeClass('active')
-                                curtabid = +@state.Send # MUST be an int
-                                nots = @props.$collapse_el.not('[data-tabid="'+ curtabid + '"]')
-                                $(el).collapse('hide') for el in nots
-                                $(@props.$collapse_el.not(nots)).collapse('show')
-                                activeClass = (el) ->
-                                        xel = $(el)
-                                        tabid_attr = +xel.attr('data-tabid') # an int
-                                        xel[if tabid_attr == curtabid then 'addClass' else 'removeClass']('active')
-                                        return
-                                activeClass(el) for el in @props.$button_el
-                                return null
-                        clicktab: (e) ->
-                                S = {}
-                                S[@props.Ksend] = +$( $(e.target).attr('href') ).attr('data-tabid') # THIS. +string makes an int
-                                S[@props.Khide] = false if @state.Hide? and @state.Hide # if the panel was hidden
-                                updates.sendClient(S)
-                                e.preventDefault()
-                                e.stopPropagation() # don't change checkbox/radio state
-                                return undefined
-                        clickhide: (e) ->
-                                (S = {})[@props.Khide] = !@state.Hide
-                                updates.sendClient(S)
-                                e.stopPropagation() # preserves checkbox/radio
-                                e.preventDefault()  # checked/selected state
-                                return undefined
-
-                @RefreshInputClass = React.createClass
-                        statics: component: (opt) ->
-                                sel = opt.sel; delete opt.$
-                                opt.$input_el = sel.find('.refresh-input')
-                                opt.$group_el = sel.find('.refresh-group')
-                                React.render(RefreshInputClass(opt), addNoscript(opt.$input_el))
-
-                        reduce: (data) ->
-                                if data?.Client? and (data.Client[@props.K]? or data.Client[@props.Kerror]?)
-                                        S = {}
-                                        S.Value = data.Client[@props.K]      if data.Client[@props.K]?
-                                        S.Error = data.Client[@props.Kerror] if data.Client[@props.Kerror]?
-                                        # console.log('newState', S)
-                                        return S
-                        getInitialState: () ->
-                                S = @reduce(Data) # a global Data
-                                # console.log('initialState', S)
-                                return S
-
-                        componentDidMount: () -> @props.$input_el.on('input', @submit)
-                        render: () ->
-                                # console.log('RefreshInputClass.render', @isMounted(), @state)
-                                @props.$input_el.prop('value', @state.Value) if @isMounted() and !@state.Error
-                                @props.$group_el[if @state.Error then 'addClass' else 'removeClass']('has-warning')
-                                return null
-                        submit: (e) ->
-                                (S = {})[@props.Ksig] = $(e.target).val()
-                                updates.sendClient(S)
-                                e.preventDefault()
-                                e.stopPropagation() # don't change checkbox/radio state
-                                return undefined
-
-                @NewTextCLASS = (reduce) -> React.createClass
-                        newstate: (data) ->
-                                v = reduce(data)
-                                return {Text: v} if v?
-                        getInitialState: () -> @newstate(Data) # a global Data
-                        render: () -> React.DOM.span(null, @state.Text)
-
-                @AlertClass = React.createClass
-                        show: () -> return @state.Error?
-                        newstate: (data) ->
-                                error = data.Client?.DebugError # data.Error
-                                a = { # return
-                                        Error: error
-                                        ErrorText: @state?.ErrorText
-                                        Changed: @state? and error? and error != @state.Error
-                                }
-                                a.ErrorText = a.Error if a.Changed and a.Error?
-                                console.log('newstate', a)
-                                return a
-                        getInitialState: () -> @newstate(Data) # a global Data
-                        render: () ->
-                                if @state.Changed
-                                        console.log('show', @state) # (if @show() then 'show' else 'hide'), @state)
-                                        @props.$collapse_el.collapse('show') if @show() # then 'show' else 'hide'
-                                return React.DOM.span(null, @state.ErrorText)
-
-                @setState = (obj, data) ->
-                        if data?
-                                delete data[key] for key of data when !data[key]?
-                                return obj.setState(data)
-
-                window.update = () -> # currentClient
-                        return if (42 for param in location.search.substr(1).split('&') when param.split('=')[0] == 'still').length
-
-                        hideconfigmem = HideClass.component({xkey: 'HideconfigMEM', $collapse_el: $('#memconfig'), $button_el: $('header a[href="#mem"]'), reverseActive: true})
-                        hideconfigif  = HideClass.component({xkey: 'HideconfigIF',  $collapse_el: $('#ifconfig'),  $button_el: $('header a[href="#if"]'),  reverseActive: true})
-                        hideconfigcpu = HideClass.component({xkey: 'HideconfigCPU', $collapse_el: $('#cpuconfig'), $button_el: $('header a[href="#cpu"]'), reverseActive: true})
-                        hideconfigdf  = HideClass.component({xkey: 'HideconfigDF',  $collapse_el: $('#dfconfig'),  $button_el: $('header a[href="#df"]'),  reverseActive: true})
-                        hideconfigps  = HideClass.component({xkey: 'HideconfigPS',  $collapse_el: $('#psconfig'),  $button_el: $('header a[href="#ps"]'),  reverseActive: true})
-                        hideconfigvg  = HideClass.component({xkey: 'HideconfigVG',  $collapse_el: $('#vgconfig'),  $button_el: $('header a[href="#vg"]'),  reverseActive: true})
-
-                        hideram = HideClass.component({xkey: 'HideRAM', $collapse_el: $('#mem'), $button_el: $('#memconfig').find('.hiding')})
-                        hidecpu = HideClass.component({xkey: 'HideCPU', $collapse_el: $('#cpu'), $button_el: $('#cpuconfig').find('.hiding')})
-                        hideps  = HideClass.component({xkey: 'HidePS',  $collapse_el: $('#ps'),  $button_el: $('#psconfig') .find('.hiding')})
-                        hidevg  = HideClass.component({xkey: 'HideVG',  $collapse_el: $('#vg'),  $button_el: $('#vgconfig') .find('.hiding')})
-
-                        ip       = React.render(NewTextCLASS((data) -> data?.IP       )(), $('#ip'      )   .get(0))
-                        hostname = React.render(NewTextCLASS((data) -> data?.Hostname )(), $('#hostname')   .get(0))
-                        uptime   = React.render(NewTextCLASS((data) -> data?.Uptime   )(), $('#uptime'  )   .get(0))
-                        la       = React.render(NewTextCLASS((data) -> data?.LA       )(), $('#la'      )   .get(0))
-
-                        iftitle  = React.render(NewTextCLASS((data) -> data?.Client?.TabTitleIF)(), $('header a[href="#if"]').get(0))
-                        dftitle  = React.render(NewTextCLASS((data) -> data?.Client?.TabTitleDF)(), $('header a[href="#df"]').get(0))
-
-                        psplus   = React.render(NewTextCLASS((data) -> data?.Client?.PSplusText)(), $('label.more[href="#psmore"]').get(0))
-                        psmore   = ButtonClass.component({Ksig: 'MorePsignal', Vsig: true,  Khide: 'HidePS', Kable: 'PSnotExpandable',  $button_el: $('label.more[href="#psmore"]')})
-                        psless   = ButtonClass.component({Ksig: 'MorePsignal', Vsig: false, Khide: 'HidePS', Kable: 'PSnotDecreasable', $button_el: $('label.less[href="#psless"]')})
-
-                        hideswap = ButtonClass.component({Khide: 'HideRAM', Ksend: 'HideSWAP', $button_el: $('label[href="#hideswap"]')})
-
-                        expandif = ButtonClass.component({Khide: 'HideIF',  Ksend: 'ExpandIF',  Ktext: 'ExpandtextIF',  Kable: 'ExpandableIF',  $button_el: $('label[href="#if"]')})
-                        expandcpu= ButtonClass.component({Khide: 'HideCPU', Ksend: 'ExpandCPU', Ktext: 'ExpandtextCPU', Kable: 'ExpandableCPU', $button_el: $('label[href="#cpu"]')})
-                        expanddf = ButtonClass.component({Khide: 'HideDF',  Ksend: 'ExpandDF',  Ktext: 'ExpandtextDF',  Kalbe: 'ExpandableDF',  $button_el: $('label[href="#df"]')})
-
-                        # NB buttons and collapses selected by class
-                        tabsif = TabsClass.component({Khide: 'HideIF', Ksend: 'TabIF', $collapse_el: $('.if-tab'), $button_el: $('.if-switch'), $hidebutton_el: $('#ifconfig').find('.hiding')})
-                        tabsdf = TabsClass.component({Khide: 'HideDF', Ksend: 'TabDF', $collapse_el: $('.df-tab'), $button_el: $('.df-switch'), $hidebutton_el: $('#dfconfig').find('.hiding')})
-
-                        refresh_mem = RefreshInputClass.component({K: 'RefreshMEM', Kerror: 'RefreshErrorMEM', Ksig: 'RefreshSignalMEM', sel: $('#memconfig')})
-                        refresh_if  = RefreshInputClass.component({K: 'RefreshIF',  Kerror: 'RefreshErrorIF',  Ksig: 'RefreshSignalIF',  sel: $('#ifconfig')})
-                        refresh_cpu = RefreshInputClass.component({K: 'RefreshCPU', Kerror: 'RefreshErrorCPU', Ksig: 'RefreshSignalCPU', sel: $('#cpuconfig')})
-                        refresh_df  = RefreshInputClass.component({K: 'RefreshDF',  Kerror: 'RefreshErrorDF',  Ksig: 'RefreshSignalDF',  sel: $('#dfconfig')})
-                        refresh_ps  = RefreshInputClass.component({K: 'RefreshPS',  Kerror: 'RefreshErrorPS',  Ksig: 'RefreshSignalPS',  sel: $('#psconfig')})
-                        refresh_vg  = RefreshInputClass.component({K: 'RefreshVG',  Kerror: 'RefreshErrorVG',  Ksig: 'RefreshSignalVG',  sel: $('#vgconfig')})
-
-                        memtable  = React.render(MEMtableCLASS(),  document.getElementById('mem'       +'-'+ 'table'))
-                        pstable   = React.render(PStableCLASS(),   document.getElementById('ps'        +'-'+ 'table'))
-                        dfbytes   = React.render(DFbytesCLASS(),   document.getElementById('dfbytes'   +'-'+ 'table'))
-                        dfinodes  = React.render(DFinodesCLASS(),  document.getElementById('dfinodes'  +'-'+ 'table'))
-                        cputable  = React.render(CPUtableCLASS(),  document.getElementById('cpu'       +'-'+ 'table'))
-                        ifbytes   = React.render(IFbytesCLASS(),   document.getElementById('ifbytes'   +'-'+ 'table'))
-                        iferrors  = React.render(IFerrorsCLASS(),  document.getElementById('iferrors'  +'-'+ 'table'))
-                        ifpackets = React.render(IFpacketsCLASS(), document.getElementById('ifpackets' +'-'+ 'table'))
-                        vgtable   = React.render(VGtableCLASS(),   document.getElementById('vg'        +'-'+ 'table'))
-
-                        # alertComp = React.render(AlertClass({
-                        #         $collapse_el: $('#alert-parent')
-                        #         }), document.getElementById('alert-message'))
-
-                        onmessage = (event) ->
-                                data = JSON.parse(event.data)
-                                return if !data?
-
-                                # alertComp.setState(alertComp.newstate(data))
-                                # if alertComp.show()
-                                #         return
-
-                                console.log('DEBUG ERROR', data.Client.DebugError) if data.Client?.DebugError?
-                                if data.Reload? and data.Reload
-                                        window.setTimeout((() -> location.reload(true)), 5000)
-                                        window.setTimeout(updates.close, 2000)
-                                        console.log('in 5s: location.reload(true)')
-                                        console.log('in 2s: updates.close()')
-                                        return
-
-                                setState(pstable,  {PStable:  data.PStable,  PSlinks: data.PSlinks})
-                                setState(dfbytes,  {DFbytes:  data.DFbytes,  DFlinks: data.DFlinks})
-                                setState(dfinodes, {DFinodes: data.DFinodes, DFlinks: data.DFlinks})
-
-                                setState(hideconfigmem, hideconfigmem.reduce(data))
-                                setState(hideconfigif,  hideconfigif .reduce(data))
-                                setState(hideconfigcpu, hideconfigcpu.reduce(data))
-                                setState(hideconfigdf,  hideconfigdf .reduce(data))
-                                setState(hideconfigps,  hideconfigps .reduce(data))
-                                setState(hideconfigvg,  hideconfigvg .reduce(data))
-
-                                setState(hideram,       hideram      .reduce(data))
-                                setState(hidecpu,       hidecpu      .reduce(data))
-                                setState(hideps,        hideps       .reduce(data))
-                                setState(hidevg,        hidevg       .reduce(data))
-
-                                setState(ip,        ip      .newstate(data))
-                                setState(hostname,  hostname.newstate(data))
-                                setState(uptime,    uptime  .newstate(data))
-                                setState(la,        la      .newstate(data))
-
-                                setState(iftitle,   iftitle .newstate(data))
-                                setState(dftitle,   dftitle .newstate(data))
-
-                                setState(psplus,    psplus  .newstate(data))
-                                setState(psmore,    psmore  .reduce(data))
-                                setState(psless,    psless  .reduce(data))
-
-                                setState(hideswap,  hideswap.reduce(data))
-
-                                setState(expandif,  expandif.reduce(data))
-                                setState(expandcpu, expandcpu.reduce(data))
-                                setState(expanddf,  expanddf.reduce(data))
-
-                                setState(tabsif,    tabsif.reduce(data))
-                                setState(tabsdf,    tabsdf.reduce(data))
-
-                                setState(refresh_mem, refresh_mem.reduce(data))
-                                setState(refresh_if,  refresh_if .reduce(data))
-                                setState(refresh_cpu, refresh_cpu.reduce(data))
-                                setState(refresh_df,  refresh_df .reduce(data))
-                                setState(refresh_ps,  refresh_ps .reduce(data))
-                                setState(refresh_vg,  refresh_vg .reduce(data))
-
-                                setState(memtable,  data.MEM)
-                                setState(cputable,  data.CPU)
-                                setState(ifbytes,   data.IFbytes)
-                                setState(iferrors,  data.IFerrors)
-                                setState(ifpackets, data.IFpackets)
-                                setState(vgtable, {
-                                    VagrantMachines: data.VagrantMachines,
-                                    VagrantError:    data.VagrantError,
-                                    VagrantErrord:   data.VagrantErrord
-                                })
-
-                                console.log(JSON.stringify(data.Client), 'recvClient') if data.Client?
-
-                              # currentClient = React.addons.update(currentClient, {$merge: data.Client}) if data.Client?
-                              # data.Client = currentClient
-
-                                # update the tooltips
-                                $('span .tooltipable')    .popover({trigger: 'hover focus'})
-                                $('span .tooltipabledots').popover() # the clickable dots
-                                return
-
-                        @updates = newwebsocket(onmessage)
-                      # @updates = neweventsource(onmessage)
-                        return
+  shim: {bootstrap: {deps: ['jquery']}}
+  baseUrl: '/js/devel'
+  paths:
+    domReady:  'vendor/min/requirejs-domReady/2.0.1/domReady'
+    headroom:  'vendor/min/headroom/0.7.0/headroom.min'
+    jquery:    'vendor/min/jquery/2.1.3/jquery-2.1.3.min'
+    bootstrap: 'vendor/min/bootstrap/3.3.2/bootstrap.min'
+    react:     'vendor/min/react/0.12.2/react.min'
+    jscript:   'gen/jscript'
+
+require ['jquery', 'bootstrap', 'react', 'jscript'], ($, _, React, jscript) ->
+  @neweventsource = (onmessage) ->
+    conn = null
+    sendSearch = (search) ->
+      # conn = new EventSource('/index.sse' + search)
+      # location.search = search
+      console.log('SEARCH', search)
+      conn.close() # if conn?
+      window.setTimeout(init, 1000)
+    sendClient = (client) ->
+      # TODO
+      return
+      console.log(JSON.stringify(client), 'sendClient')
+      obj = {Client: client}
+      # 0 conn.CONNECTING
+      # 1 conn.OPEN
+      # 2 conn.CLOSING
+      # 3 conn.CLOSED
+      if !conn? ||
+         conn.readyState == conn.CLOSING ||
+         conn.readyState == conn.CLOSED
+        init()
+      if !conn? ||
+         conn.readyState != conn.OPEN
+        console.log('Not connected, cannot send', obj)
+        return
+      return conn.send(JSON.stringify(obj))
+    init = () ->
+      conn = new EventSource('/index.sse' + location.search)
+      conn.onopen = () ->
+        $(window).bind('popstate', (() ->
+          sendSearch(location.search)
+          return))
+        return
+
+      statesel = 'table thead tr .header a.state'
+      again = (e) ->
+        $(statesel).unbind('click')
+        window.setTimeout(init, 5000) if !e.wasClean
+        return
+
+      conn.onclose   = () -> console.log('sse closed (should recover)')
+      conn.onerror   = () -> console.log('sse errord (should recover)')
+      conn.onmessage = onmessage
+
+      $(statesel).click(() ->
+        history.pushState({path: @path}, '', @href)
+        sendSearch(@search)
+        return false)
+      return
+
+    init()
+    return {
+      sendClient: sendClient
+      sendSearch: sendSearch
+      close: () -> conn.close()
+    }
+  @newwebsocket = (onmessage) ->
+    conn = null
+    sendSearch = (search) -> sendJSON({Search: search})
+    sendClient = (client) ->
+      console.log(JSON.stringify(client), 'sendClient')
+      return sendJSON({Client: client})
+    sendJSON = (obj) ->
+      # 0 conn.CONNECTING
+      # 1 conn.OPEN
+      # 2 conn.CLOSING
+      # 3 conn.CLOSED
+      if !conn? ||
+         conn.readyState == conn.CLOSING ||
+         conn.readyState == conn.CLOSED
+        init()
+      if !conn? ||
+         conn.readyState != conn.OPEN
+        console.log('Not connected, cannot send', obj)
+        return
+      return conn.send(JSON.stringify(obj))
+    init = () ->
+      hostport = window.location.hostname +
+        (if location.port then ':' + location.port else '')
+      conn = new WebSocket('ws://' + hostport + '/index.ws')
+      conn.onopen = () ->
+        sendSearch(location.search)
+        $(window).bind('popstate', (() ->
+          sendSearch(location.search)
+          return))
+        return
+
+      statesel = 'table thead tr .header a.state'
+      again = (e) ->
+        $(statesel).unbind('click')
+        window.setTimeout(init, 5000) if !e.wasClean
+        return
+
+      conn.onclose   = again
+      conn.onerror   = again
+      conn.onmessage = onmessage
+
+      $(statesel).click(() ->
+        history.pushState({path: @path}, '', @href)
+        sendSearch(@search)
+        return false)
+      return
+
+    init()
+    return {
+      sendClient: sendClient
+      sendSearch: sendSearch
+      close: () -> conn.close()
+    }
+
+  @IFbytesCLASS = React.createClass
+    getInitialState: () -> Data.IFbytes # a global Data
+    render: () ->
+      Data = {IFbytes: @state}
+      return jscript.ifbytes_table(Data, (jscript.ifbytes_rows(Data, $if
+      ) for $if in Data?.IFbytes?.List ? []))
+
+  @IFerrorsCLASS = React.createClass
+    getInitialState: () -> Data.IFerrors # a global Data
+    render: () ->
+      Data = {IFerrors: @state}
+      return jscript.iferrors_table(Data, (jscript.iferrors_rows(Data, $if
+      ) for $if in Data?.IFerrors?.List ? []))
+
+  @IFpacketsCLASS = React.createClass
+    getInitialState: () -> Data.IFpackets # a global Data
+    render: () ->
+      Data = {IFpackets: @state}
+      return jscript.ifpackets_table(Data, (jscript.ifpackets_rows(Data, $if
+      ) for $if in Data?.IFpackets?.List ? []))
+
+  @DFbytesCLASS = React.createClass
+    getInitialState: () -> {
+      DFlinks: Data.DFlinks, # a global Data
+      DFbytes: Data.DFbytes  # a global Data
+    }
+    render: () ->
+      Data = @state
+      return jscript.dfbytes_table(Data, (jscript.dfbytes_rows(Data, $disk
+      ) for $disk in Data?.DFbytes?.List ? []))
+
+  @DFinodesCLASS = React.createClass
+    getInitialState: () -> {
+      DFlinks:  Data.DFlinks, # a global Data
+      DFinodes: Data.DFinodes # a global Data
+    }
+    render: () ->
+      Data = @state
+      return jscript.dfinodes_table(Data, (jscript.dfinodes_rows(Data, $disk
+      ) for $disk in Data?.DFinodes?.List ? []))
+
+  @MEMtableCLASS = React.createClass
+    getInitialState: () -> Data.MEM # a global Data
+    render: () ->
+      Data = {MEM: @state}
+      return jscript.mem_table(Data, (jscript.mem_rows(Data, $mem
+      ) for $mem in Data?.MEM?.List ? []))
+
+  @CPUtableCLASS = React.createClass
+    getInitialState: () -> Data.CPU # a global Data
+    render: () ->
+      Data = {CPU: @state}
+      return jscript.cpu_table(Data, (jscript.cpu_rows(Data, $core
+      ) for $core in Data?.CPU?.List ? []))
+
+  @PStableCLASS = React.createClass
+    getInitialState: () -> {
+      PStable: Data.PStable, # a global Data
+      PSlinks: Data.PSlinks  # a global Data
+    }
+    render: () ->
+      Data = @state
+      return jscript.ps_table(Data, (jscript.ps_rows(Data, $proc
+      ) for $proc in Data?.PStable?.List ? []))
+
+  @VGtableCLASS = React.createClass
+    getInitialState: () -> { # a global Data:
+      VagrantMachines: Data.VagrantMachines
+      VagrantError:    Data.VagrantError
+      VagrantErrord:   Data.VagrantErrord
+    }
+    render: () ->
+      Data = @state
+      if Data?.VagrantErrord? and Data.VagrantErrord
+        rows = [jscript.vagrant_error(Data)]
+      else
+        rows = (jscript.vagrant_rows(Data, $mach
+        ) for $mach in Data?.VagrantMachines?.List ? [])
+      return jscript.vagrant_table(Data, rows)
+
+  @addNoscript = ($) -> $.append('<noscript />').find('noscript').get(0)
+
+  @HideClass = React.createClass
+    statics: component: (opt) -> React.render(
+      HideClass(opt), addNoscript(opt.$button_el))
+
+    reduce: (data) ->
+      if data?.Client?
+        value = data.Client[@props.xkey]
+        return {Hide: value} if value isnt undefined
+      return null
+    getInitialState: () -> @reduce(Data) # a global Data
+    componentDidMount: () -> @props.$button_el.click(@click)
+    render: () ->
+      @props.$collapse_el.collapse(if @state.Hide then 'hide' else 'show')
+      buttonactive =  @state.Hide
+      buttonactive = !@state.Hide if (
+        @props.reverseActive? and @props.reverseActive)
+      @props.$button_el[if buttonactive then 'addClass' else 'removeClass']('active')
+      return null
+    click: (e) ->
+      (S = {})[@props.xkey] = !@state.Hide
+      updates.sendClient(S)
+      e.stopPropagation() # preserves checkbox/radio
+      e.preventDefault()  # checked/selected state
+      return undefined
+
+  @ButtonClass = React.createClass
+    statics: component: (opt) -> React.render(
+      ButtonClass(opt), addNoscript(opt.$button_el))
+
+    reduce: (data) ->
+      if data?.Client?
+        S = {}
+        S.Hide = data.Client[@props.Khide] if                   data.Client[@props.Khide] isnt undefined # Khide is a required prop
+        S.Able = data.Client[@props.Kable] if @props.Kable? and data.Client[@props.Kable] isnt undefined
+        S.Send = data.Client[@props.Ksend] if @props.Ksend? and data.Client[@props.Ksend] isnt undefined
+        S.Text = data.Client[@props.Ktext] if @props.Ktext? and data.Client[@props.Ktext] isnt undefined
+        return S
+    getInitialState: () -> @reduce(Data) # a global Data
+    componentDidMount: () -> @props.$button_el.click(@click)
+    render: () ->
+      if @props.Kable
+        able = @state.Able
+        able = !able if not (@props.Kable.indexOf('not') > -1) # That's a hack
+        @props.$button_el.prop('disabled', able)
+        @props.$button_el[if able then 'addClass' else 'removeClass']('disabled')
+      @props.$button_el[if @state.Send then 'addClass' else 'removeClass']('active') if @props.Ksend?
+      @props.$button_el.text(@state.Text) if @props.Ktext?
+      return null
+    click: (e) ->
+      S = {}
+      S[@props.Khide] = !@state.Hide if @state.Hide?  and @state.Hide # if the panel was hidden
+      S[@props.Ksend] = !@state.Send if @props.Ksend? and @state.Send? # Q is a @state.Send? check excessive?
+      S[@props.Ksig]  =  @props.Vsig if @props.Ksig?
+      updates.sendClient(S)
+      e.stopPropagation() # preserves checkbox/radio
+      e.preventDefault()  # checked/selected state
+      return undefined
+
+  @TabsClass = React.createClass
+    statics: component: (opt) -> React.render(
+      TabsClass(opt), addNoscript(opt.$button_el))
+
+    reduce: (data) ->
+      if data?.Client?
+        S = {}
+        S.Hide = data.Client[@props.Khide] if                   data.Client[@props.Khide] isnt undefined # Khide is a required prop
+        S.Send = data.Client[@props.Ksend] if @props.Ksend? and data.Client[@props.Ksend] isnt undefined
+        return S
+    getInitialState: () -> @reduce(Data) # a global Data
+    componentDidMount: () ->
+      @props.$button_el.click(@clicktab)
+      @props.$hidebutton_el.click(@clickhide)
+    render: () ->
+      if @state.Hide
+        @props.$collapse_el.collapse('hide')
+        @props.$hidebutton_el.addClass('active')
+        return null
+      @props.$hidebutton_el.removeClass('active')
+      curtabid = +@state.Send # MUST be an int
+      nots = @props.$collapse_el.not('[data-tabid="'+ curtabid + '"]')
+      $(el).collapse('hide') for el in nots
+      $(@props.$collapse_el.not(nots)).collapse('show')
+      activeClass = (el) ->
+        xel = $(el)
+        tabid_attr = +xel.attr('data-tabid') # an int
+        xel[if tabid_attr == curtabid then 'addClass' else 'removeClass']('active')
+        return
+      activeClass(el) for el in @props.$button_el
+      return null
+    clicktab: (e) ->
+      S = {}
+      S[@props.Ksend] = +$( $(e.target).attr('href') ).attr('data-tabid') # THIS. +string makes an int
+      S[@props.Khide] = false if @state.Hide? and @state.Hide # if the panel was hidden
+      updates.sendClient(S)
+      e.preventDefault()
+      e.stopPropagation() # don't change checkbox/radio state
+      return undefined
+    clickhide: (e) ->
+      (S = {})[@props.Khide] = !@state.Hide
+      updates.sendClient(S)
+      e.stopPropagation() # preserves checkbox/radio
+      e.preventDefault()  # checked/selected state
+      return undefined
+
+  @RefreshInputClass = React.createClass
+    statics: component: (opt) ->
+      sel = opt.sel; delete opt.$
+      opt.$input_el = sel.find('.refresh-input')
+      opt.$group_el = sel.find('.refresh-group')
+      React.render(RefreshInputClass(opt), addNoscript(opt.$input_el))
+
+    reduce: (data) ->
+      if data?.Client? and (data.Client[@props.K]? or data.Client[@props.Kerror]?)
+        S = {}
+        S.Value = data.Client[@props.K]      if data.Client[@props.K]?
+        S.Error = data.Client[@props.Kerror] if data.Client[@props.Kerror]?
+        # console.log('newState', S)
+        return S
+    getInitialState: () ->
+      S = @reduce(Data) # a global Data
+      # console.log('initialState', S)
+      return S
+
+    componentDidMount: () -> @props.$input_el.on('input', @submit)
+    render: () ->
+      # console.log('RefreshInputClass.render', @isMounted(), @state)
+      @props.$input_el.prop('value', @state.Value) if @isMounted() and !@state.Error
+      @props.$group_el[if @state.Error then 'addClass' else 'removeClass']('has-warning')
+      return null
+    submit: (e) ->
+      (S = {})[@props.Ksig] = $(e.target).val()
+      updates.sendClient(S)
+      e.preventDefault()
+      e.stopPropagation() # don't change checkbox/radio state
+      return undefined
+
+  @NewTextCLASS = (reduce) -> React.createClass
+    newstate: (data) ->
+      v = reduce(data)
+      return {Text: v} if v?
+    getInitialState: () -> @newstate(Data) # a global Data
+    render: () -> React.DOM.span(null, @state.Text)
+
+  @AlertClass = React.createClass
+    show: () -> return @state.Error?
+    newstate: (data) ->
+      error = data.Client?.DebugError # data.Error
+      a = { # return
+        Error: error
+        ErrorText: @state?.ErrorText
+        Changed: @state? and error? and error != @state.Error
+      }
+      a.ErrorText = a.Error if a.Changed and a.Error?
+      console.log('newstate', a)
+      return a
+    getInitialState: () -> @newstate(Data) # a global Data
+    render: () ->
+      if @state.Changed
+        console.log('show', @state) # (if @show() then 'show' else 'hide'), @state)
+        @props.$collapse_el.collapse('show') if @show() # then 'show' else 'hide'
+      return React.DOM.span(null, @state.ErrorText)
+
+  @setState = (obj, data) ->
+    if data?
+      delete data[key] for key of data when !data[key]?
+      return obj.setState(data)
+
+  window.update = () -> # currentClient
+    return if (42 for param in location.search.substr(1).split('&') when (
+      param.split('=')[0] == 'still')).length
+
+    hideconfigmem = HideClass.component({xkey: 'HideconfigMEM', $collapse_el: $('#memconfig'), $button_el: $('header a[href="#mem"]'), reverseActive: true})
+    hideconfigif  = HideClass.component({xkey: 'HideconfigIF',  $collapse_el: $('#ifconfig'),  $button_el: $('header a[href="#if"]'),  reverseActive: true})
+    hideconfigcpu = HideClass.component({xkey: 'HideconfigCPU', $collapse_el: $('#cpuconfig'), $button_el: $('header a[href="#cpu"]'), reverseActive: true})
+    hideconfigdf  = HideClass.component({xkey: 'HideconfigDF',  $collapse_el: $('#dfconfig'),  $button_el: $('header a[href="#df"]'),  reverseActive: true})
+    hideconfigps  = HideClass.component({xkey: 'HideconfigPS',  $collapse_el: $('#psconfig'),  $button_el: $('header a[href="#ps"]'),  reverseActive: true})
+    hideconfigvg  = HideClass.component({xkey: 'HideconfigVG',  $collapse_el: $('#vgconfig'),  $button_el: $('header a[href="#vg"]'),  reverseActive: true})
+
+    hideram = HideClass.component({xkey: 'HideRAM', $collapse_el: $('#mem'), $button_el: $('#memconfig').find('.hiding')})
+    hidecpu = HideClass.component({xkey: 'HideCPU', $collapse_el: $('#cpu'), $button_el: $('#cpuconfig').find('.hiding')})
+    hideps  = HideClass.component({xkey: 'HidePS',  $collapse_el: $('#ps'),  $button_el: $('#psconfig') .find('.hiding')})
+    hidevg  = HideClass.component({xkey: 'HideVG',  $collapse_el: $('#vg'),  $button_el: $('#vgconfig') .find('.hiding')})
+
+    ip       = React.render(NewTextCLASS((data) -> data?.IP       )(), $('#ip'      )   .get(0))
+    hostname = React.render(NewTextCLASS((data) -> data?.Hostname )(), $('#hostname')   .get(0))
+    uptime   = React.render(NewTextCLASS((data) -> data?.Uptime   )(), $('#uptime'  )   .get(0))
+    la       = React.render(NewTextCLASS((data) -> data?.LA       )(), $('#la'      )   .get(0))
+
+    iftitle  = React.render(NewTextCLASS((data) -> data?.Client?.TabTitleIF)(), $('header a[href="#if"]').get(0))
+    dftitle  = React.render(NewTextCLASS((data) -> data?.Client?.TabTitleDF)(), $('header a[href="#df"]').get(0))
+
+    psplus   = React.render(NewTextCLASS((data) -> data?.Client?.PSplusText)(), $('label.more[href="#psmore"]').get(0))
+    psmore   = ButtonClass.component({Ksig: 'MorePsignal', Vsig: true,  Khide: 'HidePS', Kable: 'PSnotExpandable',  $button_el: $('label.more[href="#psmore"]')})
+    psless   = ButtonClass.component({Ksig: 'MorePsignal', Vsig: false, Khide: 'HidePS', Kable: 'PSnotDecreasable', $button_el: $('label.less[href="#psless"]')})
+
+    hideswap = ButtonClass.component({Khide: 'HideRAM', Ksend: 'HideSWAP', $button_el: $('label[href="#hideswap"]')})
+
+    expandif = ButtonClass.component({Khide: 'HideIF',  Ksend: 'ExpandIF',  Ktext: 'ExpandtextIF',  Kable: 'ExpandableIF',  $button_el: $('label[href="#if"]')})
+    expandcpu= ButtonClass.component({Khide: 'HideCPU', Ksend: 'ExpandCPU', Ktext: 'ExpandtextCPU', Kable: 'ExpandableCPU', $button_el: $('label[href="#cpu"]')})
+    expanddf = ButtonClass.component({Khide: 'HideDF',  Ksend: 'ExpandDF',  Ktext: 'ExpandtextDF',  Kalbe: 'ExpandableDF',  $button_el: $('label[href="#df"]')})
+
+    # NB buttons and collapses selected by class
+    tabsif = TabsClass.component({Khide: 'HideIF', Ksend: 'TabIF', $collapse_el: $('.if-tab'), $button_el: $('.if-switch'), $hidebutton_el: $('#ifconfig').find('.hiding')})
+    tabsdf = TabsClass.component({Khide: 'HideDF', Ksend: 'TabDF', $collapse_el: $('.df-tab'), $button_el: $('.df-switch'), $hidebutton_el: $('#dfconfig').find('.hiding')})
+
+    refresh_mem = RefreshInputClass.component({K: 'RefreshMEM', Kerror: 'RefreshErrorMEM', Ksig: 'RefreshSignalMEM', sel: $('#memconfig')})
+    refresh_if  = RefreshInputClass.component({K: 'RefreshIF',  Kerror: 'RefreshErrorIF',  Ksig: 'RefreshSignalIF',  sel: $('#ifconfig')})
+    refresh_cpu = RefreshInputClass.component({K: 'RefreshCPU', Kerror: 'RefreshErrorCPU', Ksig: 'RefreshSignalCPU', sel: $('#cpuconfig')})
+    refresh_df  = RefreshInputClass.component({K: 'RefreshDF',  Kerror: 'RefreshErrorDF',  Ksig: 'RefreshSignalDF',  sel: $('#dfconfig')})
+    refresh_ps  = RefreshInputClass.component({K: 'RefreshPS',  Kerror: 'RefreshErrorPS',  Ksig: 'RefreshSignalPS',  sel: $('#psconfig')})
+    refresh_vg  = RefreshInputClass.component({K: 'RefreshVG',  Kerror: 'RefreshErrorVG',  Ksig: 'RefreshSignalVG',  sel: $('#vgconfig')})
+
+    memtable  = React.render(MEMtableCLASS(),  document.getElementById('mem'       +'-'+ 'table'))
+    pstable   = React.render(PStableCLASS(),   document.getElementById('ps'        +'-'+ 'table'))
+    dfbytes   = React.render(DFbytesCLASS(),   document.getElementById('dfbytes'   +'-'+ 'table'))
+    dfinodes  = React.render(DFinodesCLASS(),  document.getElementById('dfinodes'  +'-'+ 'table'))
+    cputable  = React.render(CPUtableCLASS(),  document.getElementById('cpu'       +'-'+ 'table'))
+    ifbytes   = React.render(IFbytesCLASS(),   document.getElementById('ifbytes'   +'-'+ 'table'))
+    iferrors  = React.render(IFerrorsCLASS(),  document.getElementById('iferrors'  +'-'+ 'table'))
+    ifpackets = React.render(IFpacketsCLASS(), document.getElementById('ifpackets' +'-'+ 'table'))
+    vgtable   = React.render(VGtableCLASS(),   document.getElementById('vg'        +'-'+ 'table'))
+
+    # alertComp = React.render(AlertClass({
+    #         $collapse_el: $('#alert-parent')
+    #         }), document.getElementById('alert-message'))
+
+    onmessage = (event) ->
+      data = JSON.parse(event.data)
+      return if !data?
+
+      # alertComp.setState(alertComp.newstate(data))
+      # if alertComp.show()
+      #         return
+
+      console.log('DEBUG ERROR',
+        data.Client.DebugError) if data.Client?.DebugError?
+      if data.Reload? and data.Reload
+        window.setTimeout((() -> location.reload(true)), 5000)
+        window.setTimeout(updates.close, 2000)
+        console.log('in 5s: location.reload(true)')
+        console.log('in 2s: updates.close()')
+        return
+
+      setState(pstable,  {PStable:  data.PStable,  PSlinks: data.PSlinks})
+      setState(dfbytes,  {DFbytes:  data.DFbytes,  DFlinks: data.DFlinks})
+      setState(dfinodes, {DFinodes: data.DFinodes, DFlinks: data.DFlinks})
+
+      setState(hideconfigmem, hideconfigmem.reduce(data))
+      setState(hideconfigif,  hideconfigif .reduce(data))
+      setState(hideconfigcpu, hideconfigcpu.reduce(data))
+      setState(hideconfigdf,  hideconfigdf .reduce(data))
+      setState(hideconfigps,  hideconfigps .reduce(data))
+      setState(hideconfigvg,  hideconfigvg .reduce(data))
+
+      setState(hideram,       hideram      .reduce(data))
+      setState(hidecpu,       hidecpu      .reduce(data))
+      setState(hideps,        hideps       .reduce(data))
+      setState(hidevg,        hidevg       .reduce(data))
+
+      setState(ip,        ip      .newstate(data))
+      setState(hostname,  hostname.newstate(data))
+      setState(uptime,    uptime  .newstate(data))
+      setState(la,        la      .newstate(data))
+
+      setState(iftitle,   iftitle .newstate(data))
+      setState(dftitle,   dftitle .newstate(data))
+
+      setState(psplus,    psplus  .newstate(data))
+      setState(psmore,    psmore  .reduce(data))
+      setState(psless,    psless  .reduce(data))
+
+      setState(hideswap,  hideswap.reduce(data))
+
+      setState(expandif,  expandif.reduce(data))
+      setState(expandcpu, expandcpu.reduce(data))
+      setState(expanddf,  expanddf.reduce(data))
+
+      setState(tabsif,    tabsif.reduce(data))
+      setState(tabsdf,    tabsdf.reduce(data))
+
+      setState(refresh_mem, refresh_mem.reduce(data))
+      setState(refresh_if,  refresh_if .reduce(data))
+      setState(refresh_cpu, refresh_cpu.reduce(data))
+      setState(refresh_df,  refresh_df .reduce(data))
+      setState(refresh_ps,  refresh_ps .reduce(data))
+      setState(refresh_vg,  refresh_vg .reduce(data))
+
+      setState(memtable,  data.MEM)
+      setState(cputable,  data.CPU)
+      setState(ifbytes,   data.IFbytes)
+      setState(iferrors,  data.IFerrors)
+      setState(ifpackets, data.IFpackets)
+      setState(vgtable, {
+        VagrantMachines: data.VagrantMachines,
+        VagrantError:    data.VagrantError,
+        VagrantErrord:   data.VagrantErrord
+      })
+
+      console.log(JSON.stringify(data.Client), 'recvClient') if data.Client?
+
+    # currentClient = React.addons.update(currentClient, {$merge: data.Client}) if data.Client?
+    # data.Client = currentClient
+
+      # update the tooltips
+      $('span .tooltipable')    .popover({trigger: 'hover focus'})
+      $('span .tooltipabledots').popover() # the clickable dots
+      return
+
+    @updates = newwebsocket(onmessage)
+  # @updates = neweventsource(onmessage)
+    return

 @ready = require ['jquery', 'bootstrap', 'headroom'], ($) ->
-        # neither bootstrap nor headroom export anything
-        (new window.Headroom(document.querySelector('nav'), {
-                offset: 20 # ~padding-top of a container row
-        })).init()
-
-        $('.collapse').collapse({toggle: false}) # init collapsable objects
-
-        $('span .tooltipable')      .popover({trigger: 'hover focus'})
-        $('span .tooltipabledots')  .popover() # the clickable dots
-        $('[data-toggle="popover"]').popover() # should be just #hostname
-        $('#la')                    .popover({
-                trigger: 'hover focus',
-                placement: 'right', # not 'auto right' until #la is the last element for it's parent
-                html: true, content: () -> $('#uptime-parent').html()
-        })
-
-        $('body').on('click', (e) -> # hide the popovers on click outside
-                $('span .tooltipabledots').each(() ->
-                        # the 'is' for buttons that trigger popups
-                        # the 'has' for icons within a button that triggers a popup
-                        $(this).popover('hide') if !$(this).is(e.target) and $(this).has(e.target).length == 0 && $('.popover').has(e.target).length == 0
-                        return)
-                return)
-
-        window.update() # (Data.Client)
-        return
+  # neither bootstrap nor headroom export anything
+  (new window.Headroom(document.querySelector('nav'), {
+    offset: 20 # ~padding-top of a container row
+  })).init()
+
+  $('.collapse').collapse({toggle: false}) # init collapsable objects
+
+  $('span .tooltipable')      .popover({trigger: 'hover focus'})
+  $('span .tooltipabledots')  .popover() # the clickable dots
+  $('[data-toggle="popover"]').popover() # should be just #hostname
+  $('#la')                    .popover({
+    trigger: 'hover focus',
+    placement: 'right', # not 'auto right' until #la is the last element for it's parent
+    html: true, content: () -> $('#uptime-parent').html()
+  })
+
+  $('body').on('click', (e) -> # hide the popovers on click outside
+    $('span .tooltipabledots').each(() ->
+      # the 'is' for buttons that trigger popups
+      # the 'has' for icons within a button that triggers a popup
+      $(this).popover('hide') if (!$(this).is(e.target) and
+        $(this).has(e.target).length == 0 and
+        $('.popover').has(e.target).length == 0)
+      return)
+    return)
+
+  window.update() # (Data.Client)
+  return

 require ['domReady'], (domReady) ->
-        domReady () -> ready()
+  domReady () -> ready()
+
+# Local variables:
+# coffee-tab-width: 2
+# End:


Show emoji popup if : preceeding by space or is new line.

--- app/assets/javascripts/gfm_auto_complete.js.coffee
+++ app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -20,7 +20,7 @@
     input = $('.js-gfm-input')

     # Emoji
-    input.atWho ':',
+    input.atWho '(?:^|\\s):',
       data: @Emoji.data
       tpl: @Emoji.template


Removed semicolons

Removed semicolons

--- app/assets/javascripts/internal/masthead.js.coffee
+++ app/assets/javascripts/internal/masthead.js.coffee
@@ -8,13 +8,13 @@
     $("#nav-search-trigger").is(":visible")

   hideNav = ->
-    $("body").removeClass("nav-open");
+    $("body").removeClass("nav-open")
     $malmoMastheadNav.removeClass("expanded")
     $(document).off 'click.nav'

   showNav = (event) ->
     hideSearch()
-    $("body").addClass("nav-open");
+    $("body").addClass("nav-open")
     $malmoMastheadNav.addClass("expanded")
     # Close on click outside the list
     $(document).on 'click.nav', (event) ->
@@ -59,7 +59,7 @@
   if profile.workingfields then hijackNav('#nav-my-workingfield', profile.workingfields, 'Mina arbetsfält')

   $("#nav-menu-trigger a").click (event) ->
-    event.preventDefault();
+    event.preventDefault()
     if $("#malmo-masthead nav.masthead-main").is(":hidden") then showNav(event) else hideNav()

   # Browser sniffing hack for landscape text size on iPhone. Note: Prevents zooming on desktops so we limit it to iOS.


checking contents of snippet vs element

checking contents of snippet vs element

--- app/assets/javascripts/mercury/regions/full.coffee
+++ app/assets/javascripts/mercury/regions/full.coffee
@@ -225,8 +225,9 @@
                 element.html(snippet.data)
             catch error
           if element.attr('wrap_content') == 'true'
-            console.log('this is correct?')
+            console.log('this is correct? '+element.html()+", other thing: "+snippet.data)
             element.data('wrappedhtml', _.escape(element.html()))
+            element.html(snippet.data)

       # set the html
       @element.html(container.html())