Awesomest monitoring UI

pull/1/merge
Thibault Duplessis 2012-05-24 01:38:24 +02:00
parent 39cf289225
commit e21c42a688
32 changed files with 522 additions and 55 deletions

View File

@ -1,34 +1,42 @@
package controllers
import lila._
import play.api.mvc._
import play.api.libs.Comet
import play.api.libs.concurrent._
import scalaz.effects._
import akka.pattern.ask
import akka.util.duration._
import akka.util.{ Duration, Timeout }
import akka.util.Timeout
import lila._
import socket.GetNbMembers
import report.{ GetStatus, GetNbPlaying }
import monitor._
object Report extends LilaController {
object Monitor extends LilaController {
val reporting = env.site.reporting
def reporting = env.monitor.reporting
implicit val timeout = Timeout(100 millis)
def status = Action {
val index = Action {
Ok(views.html.monitor.monitor(env.monitor.stream.maxMemory))
}
val stream = Action {
Ok.stream(env.monitor.stream.getData &> Comet(callback = "parent.message"))
}
val status = Action {
Async {
(reporting ? GetStatus).mapTo[String].asPromise map { Ok(_) }
}
}
def nbPlayers = Action {
val nbPlayers = Action {
Async {
(reporting ? GetNbMembers).mapTo[Int].asPromise map { Ok(_) }
}
}
def nbPlaying = Action {
val nbPlaying = Action {
Async {
(reporting ? GetNbPlaying).mapTo[Int].asPromise map { Ok(_) }
}

View File

@ -77,6 +77,10 @@ final class CoreEnv private (application: Application, val settings: Settings) {
settings = settings,
gameRepo = game.gameRepo)
lazy val monitor = new lila.monitor.MonitorEnv(
app = app,
settings = settings)
lazy val preloader = new Preload(
fisherman = lobby.fisherman,
history = lobby.history,

View File

@ -28,8 +28,8 @@ object Cron {
}
}
message(5 seconds) {
env.site.reporting -> report.Update(env)
message(1 seconds) {
env.monitor.reporting -> monitor.Update(env)
}
message(1 second) {

View File

@ -20,7 +20,7 @@ object Global extends GlobalSettings {
}
override def onRouteRequest(req: RequestHeader): Option[Handler] = {
//println(req)
env.monitor.rpsProvider.countRequest()
env.i18n.requestHandler(req) orElse super.onRouteRequest(req)
}

View File

@ -9,6 +9,8 @@ final class Settings(config: Config) {
val SiteUidTimeout = millis("site.uid.timeout")
val MonitorTimeout = millis("monitor.timeout")
val GameMessageLifetime = millis("game.message.lifetime")
val GameUidTimeout = millis("game.uid.timeout")
val GameHubTimeout = millis("game.hub.timeout")

View File

@ -0,0 +1,42 @@
package lila.monitor;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.*;
public class CPU {
private int availableProcessors = ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors();
private long lastSystemTime = 0;
private long lastProcessCpuTime = 0;
public synchronized double getCpuUsage() {
if(lastSystemTime == 0) {
baselineCounters();
return 0;
}
long systemTime = System.nanoTime();
long processCpuTime = 0;
if(ManagementFactory.getOperatingSystemMXBean() instanceof OperatingSystemMXBean) {
processCpuTime = ((OperatingSystemMXBean)ManagementFactory.getOperatingSystemMXBean()).getProcessCpuTime();
}
double cpuUsage = (double) (processCpuTime - lastProcessCpuTime) / (systemTime - lastSystemTime);
lastSystemTime = systemTime;
lastProcessCpuTime = processCpuTime;
return cpuUsage / availableProcessors;
}
private void baselineCounters() {
lastSystemTime = System.nanoTime();
if (ManagementFactory.getOperatingSystemMXBean() instanceof OperatingSystemMXBean) {
lastProcessCpuTime = ( (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean() ).getProcessCpuTime();
}
}
}

View File

@ -0,0 +1,28 @@
package lila
package monitor
import akka.actor._
import play.api.libs.concurrent._
import play.api.Application
import core.Settings
final class MonitorEnv(
app: Application,
settings: Settings) {
implicit val ctx = app
import settings._
lazy val reporting = Akka.system.actorOf(
Props(new Reporting(
rpsProvider = rpsProvider
)), name = ActorReporting)
val rpsProvider = new RpsProvider(
timeout = MonitorTimeout)
val stream = new Stream(
reporting = reporting,
timeout = MonitorTimeout)
}

View File

@ -1,5 +1,5 @@
package lila
package report
package monitor
import socket.GetNbMembers
import round.GetNbHubs
@ -14,7 +14,8 @@ import play.api.Play.current
import scala.io.Source
import java.lang.management.ManagementFactory
final class Reporting extends Actor {
final class Reporting(
rpsProvider: RpsProvider) extends Actor {
case class SiteSocket(nbMembers: Int)
case class LobbySocket(nbMembers: Int)
@ -29,26 +30,31 @@ final class Reporting extends Actor {
var site = SiteSocket(0)
var lobby = LobbySocket(0)
var game = GameSocket(0, 0)
var rps = 0
var cpu = 0
var remoteAi = false
private var displays = 0
var displays = 0
val osStats = ManagementFactory.getOperatingSystemMXBean
val threadStats = ManagementFactory.getThreadMXBean
val memoryStats = ManagementFactory.getMemoryMXBean
val cpuStats = new CPU()
implicit val executor = Akka.system.dispatcher
implicit val timeout = Timeout(100 millis)
def receive = {
case GetNbMembers sender ! allMembers
case GetNbMembers sender ! allMembers
case GetNbGames sender ! nbGames
case GetNbGames sender ! nbGames
case GetNbPlaying sender ! nbPlaying
case GetNbPlaying sender ! nbPlaying
case GetStatus sender ! status
case GetStatus sender ! status
case GetMonitorData sender ! monitorData
case Update(env) {
val before = nowMillis
@ -76,9 +82,9 @@ final class Reporting extends Actor {
loadAvg = osStats.getSystemLoadAverage.toFloat
nbThreads = threadStats.getThreadCount
memory = memoryStats.getHeapMemoryUsage.getUsed / 1024 / 1024
rps = rpsProvider.rps
cpu = ((cpuStats.getCpuUsage() * 1000).round / 10.0).toInt
remoteAi = env.ai.remoteAi.currentHealth
display()
}
} onComplete {
case Left(a) println("Reporting: " + a.getMessage)
@ -117,5 +123,37 @@ final class Reporting extends Actor {
remoteAi.fold(1, 0)
) mkString " "
private def monitorData = List(
"users" -> allMembers,
"site" -> site.nbMembers,
"lobby" -> lobby.nbMembers,
"games" -> game.nbMembers,
"hubs" -> game.nbHubs,
"recent" -> nbPlaying,
"lat" -> latency,
"thread" -> nbThreads,
"cpu" -> cpu,
"load" -> loadAvg,
"memory" -> memory,
"rps" -> rps
) map {
case (name, value) value + ":" + name
}
private def allMembers = site.nbMembers + lobby.nbMembers + game.nbMembers
object Formatter {
def dataLine(data: List[(String, Any)]) = new {
def header = data map (_._1) mkString " "
def line = data map {
case (name, value) {
val s = value.toString
List.fill(name.size - s.size)(" ").mkString + s + " "
}
} mkString
}
}
}

View File

@ -0,0 +1,30 @@
package lila
package monitor
import play.api.libs.concurrent.Promise
import java.util.concurrent.TimeUnit
import scala.concurrent.stm._
import scala.math.round
final class RpsProvider(timeout: Int) {
private val counter = Ref((0, (0, nowMillis)))
def countRequest() = {
val current = nowMillis
counter.single.transform {
case (precedent, (count, millis)) if current > millis + timeout (0, (1, current))
case (precedent, (count, millis)) if current > millis + (timeout / 2) (count, (1, current))
case (precedent, (count, millis)) (precedent, (count + 1, millis))
}
}
def rps = round {
val current = nowMillis
val (precedent, (count, millis)) = counter.single()
val since = current - millis
if (since <= timeout) ((count + precedent) * 1000) / (since + timeout / 2)
else 0
} toInt
}

View File

@ -0,0 +1,29 @@
package lila
package monitor
import play.api.libs.iteratee._
import play.api.libs.concurrent.Promise
import akka.actor._
import akka.dispatch.Await
import akka.pattern.ask
import akka.util.duration._
import akka.util.Timeout
final class Stream(
reporting: ActorRef,
timeout: Int) {
implicit val maxWait = 100 millis
implicit val maxWaitTimeout = Timeout(maxWait)
val getData = Enumerator.generateM {
Promise.timeout(Some(data mkString ";"), timeout)
}
def maxMemory = Runtime.getRuntime().totalMemory() / (1024 * 1024)
private def data = Await.result(
reporting ? GetMonitorData mapTo manifest[List[String]],
maxWait
)
}

View File

@ -1,10 +1,11 @@
package lila
package report
package monitor
import core.CoreEnv
case object GetNbGames
case object GetNbPlaying
case object GetStatus
case object GetMonitorData
case class Update(env: CoreEnv)

View File

@ -1,17 +0,0 @@
package lila
package report
object Formatter {
def dataLine(data: List[(String, Any)]) = new {
def header = data map (_._1) mkString " "
def line = data map {
case (name, value) {
val s = value.toString
List.fill(name.size - s.size)(" ").mkString + s + " "
}
} mkString
}
}

View File

@ -5,7 +5,6 @@ import akka.actor._
import play.api.libs.concurrent._
import play.api.Application
import report.Reporting
import game.GameRepo
import core.Settings
@ -17,9 +16,6 @@ final class SiteEnv(
implicit val ctx = app
import settings._
lazy val reporting = Akka.system.actorOf(
Props(new Reporting), name = ActorReporting)
lazy val hub = Akka.system.actorOf(
Props(new Hub(timeout = SiteUidTimeout)), name = ActorSiteHub)

View File

@ -18,7 +18,7 @@
<div class="box">
@if(game.isBeingPlayed) {
<a class="link" href="@routes.Round.watcher(gameId, color.name)">
@trans.playingRightNow()
@trans.playingRightNow()
</a>
} else {
@game.updatedAt.map(showDate)

View File

@ -1,6 +1,6 @@
@(title: String, active: Option[ui.SiteMenu.Elem] = None, baseline: Option[Html] = None, goodies: Option[Html] = None, chat: Option[Html] = None, robots: Boolean = true, moreCss: Html = Html(""), moreJs: Html = Html(""))(body: Html)(implicit ctx: Context)
<!DOCTYPE html>
<!doctype html>
<html lang="@lang.language">
<head>
<title>lichess @title | @trans.freeOnlineChess()</title>

View File

@ -0,0 +1,19 @@
@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/monitor.css")" />
<link rel="apple-touch-icon-precomposed" href="@routes.Assets.at("images/icon.png")"/>
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="@routes.Assets.at("images/icon.png")">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="@routes.Assets.at("images/icon@2x.png")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")" />
<script src="@routes.Assets.at("vendor/zanimo.min.js")" type="text/javascript"></script>
</head>
<body>
@content
</body>
</html>

View File

@ -0,0 +1,9 @@
@(totalMemory: Long)
@main("RPS") {
<h1>Lichess Server Monitoring <span class="down">DOWN</span></h1>
<div id="monitors" class="clearfix">
<script type="text/javascript">window.App = { }; window.App.totalMemory = @totalMemory;</script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/monitor.js")"></script>
</div>
}

View File

@ -17,6 +17,9 @@ mongo {
connectTimeout = 15 seconds
threadsAllowedToBlockForConnectionMultiplier = 500
}
monitor {
timeout = 1 second
}
lobby {
message.max = 30
entry.max = 12

View File

@ -99,9 +99,11 @@ GET /lobby/socket controllers.Lobby.socket
#POST /api/lobby/create/:hookOwnerId controllers.Lobby.create(hookOwnerId: String)
#POST /api/lobby/chat-ban/:username controllers.Lobby.chatBan(username: String)
# Reporting API
GET /nb-players controllers.Report.nbPlayers
GET /nb-playing controllers.Report.nbPlaying
GET /status controllers.Report.status
GET /assets/*file controllers.Assets.at(path="/public", file)
# Monitor
GET /monitor controllers.Monitor.index
GET /monitor/stream controllers.Monitor.stream
GET /nb-players controllers.Monitor.nbPlayers
GET /nb-playing controllers.Monitor.nbPlaying
GET /status controllers.Monitor.status

View File

@ -22,8 +22,6 @@ trait Dependencies {
val json = "com.codahale" %% "jerkson" % "0.5.0"
val guava = "com.google.guava" % "guava" % "11.0.2"
val apache = "org.apache.commons" % "commons-lang3" % "3.1"
val jodaTime = "joda-time" % "joda-time" % "2.1"
val jodaConvert = "org.joda" % "joda-convert" % "1.2"
val scalaTime = "org.scala-tools.time" %% "time" % "0.5"
val slf4jNop = "org.slf4j" % "slf4j-nop" % "1.6.4"
val dispatch = "net.databinder" %% "dispatch-http" % "0.8.7"
@ -57,8 +55,6 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
salat,
guava,
apache,
jodaTime,
jodaConvert,
scalaTime,
dispatch,
auth,

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,160 @@
(function (app) {
function create(elt) { return window.document.createElement(elt); }
function SpeedOMeter (config) {
this.maxVal = config.maxVal;
this.threshold = config.threshold || 1;
this.unit = config.unit ? config.unit + " " : "";
this.name = config.name;
this.container = config.container;
this.elt = create("div");
this.elt.className = "monitor";
var title = create("span");
title.innerHTML = this.name;
title.className = 'title';
this.elt.appendChild(title);
this.screenCurrent = create("span");
this.screenCurrent.className = 'screen current';
this.elt.appendChild(this.screenCurrent);
this.screenMax = create("span");
this.screenMax.className = 'screen max';
this.screenMax.innerHTML = this.maxVal + this.unit;
this.elt.appendChild(this.screenMax);
this.needle = create("div");
this.needle.className = "needle";
this.elt.appendChild(this.needle);
this.light = create("div");
this.light.className = "green light";
this.elt.appendChild(this.light);
var wheel = create("div");
wheel.className = "wheel";
this.elt.appendChild(wheel);
this.container.appendChild(this.elt);
}
SpeedOMeter.prototype.update = function (val) {
Zanimo.transition(
this.needle,
"transform",
"rotate(" + (val > this.maxVal ? 175 : val * 170 / this.maxVal) + "deg)",
1500,
"ease-in"
);
if (val > (this.threshold * this.maxVal)) {
this.elt.className = "monitor alert";
} else {
this.elt.className = "monitor";
}
this.screenCurrent.innerHTML = val + this.unit;
}
function init() {
var container = window.document.getElementById("monitors")
app.rps = new SpeedOMeter({
name : "RPS",
maxVal : 50,
threshold: 0.9,
container : container
});
app.memory = new SpeedOMeter({
name : "MEMORY",
maxVal : app.totalMemory,
threshold: 0.8,
unit : "MB",
container : container
});
app.cpu = new SpeedOMeter({
name : "CPU",
maxVal : 100,
threshold: 0.3,
unit : "%",
container : container
});
app.thread = new SpeedOMeter({
name : "THREAD",
maxVal : 200,
threshold: 0.5,
container : container
});
app.load = new SpeedOMeter({
name : "LOAD",
maxVal : 1,
threshold: 0.3,
container : container
});
app.lat = new SpeedOMeter({
name : "LATENCY",
maxVal : 5,
threshold: 0.5,
container : container
});
app.users = new SpeedOMeter({
name : "USERS",
maxVal : 500,
container : container
});
app.lobby = new SpeedOMeter({
name : "LOBBY",
maxVal : 100,
threshold: 1,
container : container
});
app.hubs = new SpeedOMeter({
name : "GAME",
maxVal : 300,
threshold: 1,
container : container
});
var iframe = create("iframe");
iframe.src = "/monitor/stream";
iframe.style.display = "none";
window.message = function (msg) {
var ds = msg.split(";");
app.lastCall = (new Date()).getTime();
for(var i in ds) {
var d = ds[i].split(":");
if (d.length == 2) {
if (typeof app[d[1]] != "undefined") {
app[d[1]].update(d[0]);
}
}
}
}
setTimeout(function () {
app.lastCall = (new Date()).getTime();
window.document.body.appendChild(iframe);
}, 100);
setInterval(function () {
if ((new Date()).getTime() - app.lastCall > 3000) {
window.document.body.className = "down";
} else {
window.document.body.className = "up";
}
},1100);
}
window.document.addEventListener("DOMContentLoaded", init, false);
})(window.App);

View File

@ -0,0 +1,105 @@
body {
background: url(/assets/images/monitor/background.png);
font-family: "Myriad Pro", Helvetica, Arial, Serif;
font-size: 8px;
color: black;
text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.1);
padding-top: 2px;
margin: auto;
width: 960px;
}
.clearfix:after { content:"."; display:block; height:0; clear:both; visibility:hidden; }
.clearfix {display:inline-block;}
h1 {
text-align: center;
font-size: 20px;
}
h1 .down {
color: #884444;
display: none;
}
body.down h1 .down {
display: inline;
}
.monitor {
float: left;
margin: 20px 8px 0 8px;
width: 302px;
height: 123px;
background: url(/assets/images/monitor/monitor.png);
position: relative;
}
.monitor.alert {
}
.monitor .title {
color: #d0d0d0;
font-size: 10px;
position: absolute;
top: 9px;
left: 223px;
}
.monitor .screen {
display: block;
position: absolute;
font-size: 14px;
color: #454a4e;
text-shadow: 0 1px 0px #000000;
background: black;
border-radius: 4px;
width: 60px;
height: 18px;
padding: 1px 5px 2px 5px;
margin-top: 4px;
text-align: center;
background-image: -moz-linear-gradient(top left, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
background-image: -o-linear-gradient(top left, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
background-image: -webkit-gradient(linear, 0 0, 100% 100%, from(rgba(255, 255, 255, 0.1)), to(rgba(255, 255, 255, 0)));
box-shadow: 0px 1px 1px #494e54, inset 0px 1px 2px rgba(0, 0, 0, 0.6);
}
.monitor .screen.current {
top: 28px;
left: 214px;
}
.monitor.alert .screen.current {
/*color: #884444;*/
}
.monitor .screen.max {
top: 55px;
left: 214px;
}
.wheel {
position: absolute;
width: 13px;
height: 13px;
top: 97px;
left: 99px;
background: url(/assets/images/monitor/wheel.png);
}
.needle {
-webkit-transform-origin: 100% 50%;
-o-transform-origin: 100% 50%;
-moz-transform-origin: 100% 50%;
position: absolute;
top: 101px;
left: 30px;
height: 5px;
width: 76px;
background: url(/assets/images/monitor/needle.png);
}
div.light {
width: 7px;
height: 7px;
position: absolute;
top: 11px;
left: 211px;
}
div.light.green {
background: url(/assets/images/monitor/green-light.png);
}
div.light.red,
.monitor.alert div.light {
background: url(/assets/images/monitor/red-light.png);
}

8
public/vendor/zanimo.min.js vendored 100644
View File

@ -0,0 +1,8 @@
var Zanimo=function(){var g=function(a){var f=Zanimo.async.defer();f.resolve(a);return f.promise};g.kDelta=50;g.delay=function(a,f){var b=Zanimo.async.defer();setTimeout(function(){b.resolve(f||a)},a);return b.promise};g.transition=function(a,f,b,d,c){var e=Zanimo.async.defer(),j=-1,i=!1;if(!a||!a.nodeType||!(a.nodeType>=0))return e.resolve(Zanimo.async.reject("Zanimo transition Error : no given dom Element!")),e.promise;var h=function(){i=!0;e.resolve(a);a.removeEventListener(Zanimo.utils.prefix.evt,
h,!1)};a.addEventListener(Zanimo.utils.prefix.evt,h,!1);Zanimo.delay(d+g.kDelta).then(function(){i||e.resolve(Zanimo.async.reject("Zanimo transition Error on "+a.id+" with "+f+":"+b))});j=Zanimo.utils.addTransition(a,f);Zanimo.utils.setAttributeAt(a,"TransitionDuration",d+"ms",j);Zanimo.utils.setAttributeAt(a,"TransitionTimingFunction",c||"linear",j);Zanimo.utils.setProperty(a,f,b);return e.promise};return g}();
(function(g,a){a.enqueue=function(a){setTimeout(a,1)};a.isPromise=function(a){return a&&typeof a.then==="function"};a.defer=function(){var b=[],d;return{resolve:function(c){if(b){d=f(c);for(var c=0,e=b.length;c<e;c++)(function(b){a.enqueue(function(){d.then.apply(d,b)})})(b[c]);b=void 0}},promise:{then:function(c,e){var f=a.defer(),c=c||function(a){return a},e=e||function(b){return a.reject(b)},g=function(a){f.resolve(c(a))},h=function(a){f.resolve(e(a))};b?b.push([g,h]):a.enqueue(function(){d.then(g,
h)});return f.promise}}}};var f=function(b){return b&&b.then?b:{then:function(d){var c=a.defer();a.enqueue(function(){c.resolve(d(b))});return c.promise}}};a.reject=function(b){return{then:function(d,c){var e=a.defer();a.enqueue(function(){e.resolve(c(b))});return e.promise}}};g.when=a.when=function(b,d,c){var e=a.defer(),g,d=d||function(a){return a},c=c||function(b){return a.reject(b)},i=function(b){try{return d(b)}catch(c){return a.reject(c)}},h=function(b){try{return c(b)}catch(d){return a.reject(d)}};
a.enqueue(function(){f(b).then(function(a){g||(g=!0,e.resolve(f(a).then(i,h)))},function(a){g||(g=!0,e.resolve(h(a)))})});return e.promise}})(window.Zanimo,window.Zanimo.async=window.Zanimo.async||{});
(function(g,a,f){a.prefixed=["transform"];a.prefix={webkit:{evt:"webkitTransitionEnd",name:"webkit",css:"-webkit-"},opera:{evt:"oTransitionEnd",name:"O",css:"-o-"},firefox:{evt:"transitionend",name:"Moz",css:"-moz-"}};a.browser=f.match(/.*(Chrome|Safari).*/)?"webkit":f.match(/.*Firefox.*/)?"firefox":navigator.appName==="Opera"?"opera":"webkit";a.prefix=a.prefix[a.browser];a.transitionProperty=a.prefix.name+"TransitionProperty";a.addTransition=function(b,d,c,e){d=a._prefixCSS(d);c=b.style[a.transitionProperty];
e=(c?c.split(", "):[]).indexOf(d);return e===-1?a._appendToProperty(b,"TransitionProperty",d):e};a.setAttributeAt=function(b,d,c,e){var f=(b.style[a.prefix.name+d]||"").split(",");f[e]=c;b.style[a.prefix.name+d]=f.toString()};a.setProperty=function(b,d,c){b.style[a._prefixAndCapitalize(d)]=c};a._appendToProperty=function(a,d,c){var e=a.style[d]||"";a.style[d]=e.length>0?e+", "+c:c;return a.style[d].split(", ").indexOf(c)};a._prefixCSS=function(b){return a.prefixed.indexOf(b)===-1?b:a.prefix.css+b};
a._prefixAndCapitalize=function(b){b=a._prefixCSS(b);return b.split("-").reduce(function(a,b){return a+b.charAt(0).toUpperCase()+b.substr(1)})}})(window.Zanimo,window.Zanimo.utils=window.Zanimo.utils||{},navigator.userAgent);

4
todo
View File

@ -18,6 +18,10 @@ start chess960 after both player move http://fr.lichess.org/forum/lichess-feedba
elo floor 800
use true auth for socket username
prevent round watcher/player response client caching
user info is expensive - cache it
chess960 confirmation http://fr.lichess.org/forum/lichess-feedback/separate-960-lobby?page=1#7
use play-navigator router case class MyRegexStr(value: String); implicit val MyRegexStrPathParam: PathParam[MyRegexStr] = new PathParam[MyRegexStr] { def apply(s: MyRegexStr) = s.value}; def unapply(s: String) = val Rx = "(\w+)".r; s match { case Rx(x) => Some(x); case _ => None } }
http://codetunes.com/2012/05/09/scala-dsl-tutorial-writing-web-framework-router
next deploy:
db.user.update({},{$unset:{isOnline: true}}, false, true)