Awesomest monitoring UI
|
@ -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(_) }
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
}
|
|
@ -17,6 +17,9 @@ mongo {
|
|||
connectTimeout = 15 seconds
|
||||
threadsAllowedToBlockForConnectionMultiplier = 500
|
||||
}
|
||||
monitor {
|
||||
timeout = 1 second
|
||||
}
|
||||
lobby {
|
||||
message.max = 30
|
||||
entry.max = 12
|
||||
|
|
12
conf/routes
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.8 KiB |
|
@ -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);
|
|
@ -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);
|
||||
}
|
|
@ -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
|
@ -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)
|
||||
|
|