aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/etc/varnish/default.vcl2
-rw-r--r--web/nms.gathering.org/nms2/index.html637
-rw-r--r--web/nms.gathering.org/nms2/js/nms-color-util.js36
-rw-r--r--web/nms.gathering.org/nms2/js/nms-map-handlers.js127
-rw-r--r--web/nms.gathering.org/nms2/js/nms.js499
-rwxr-xr-xweb/nms.gathering.org/port-state.pl7
-rwxr-xr-xweb/nms.gathering.org/uplinkkart-text.pl2
7 files changed, 942 insertions, 368 deletions
diff --git a/web/etc/varnish/default.vcl b/web/etc/varnish/default.vcl
index 5f825a6..b4445d1 100644
--- a/web/etc/varnish/default.vcl
+++ b/web/etc/varnish/default.vcl
@@ -95,7 +95,7 @@ sub vcl_backend_response {
set beresp.ttl = 0s;
}
if(bereq.url ~ "port-state.pl" && beresp.status == 200) {
- set beresp.ttl = 30s;
+ set beresp.ttl = 1s;
}
if (beresp.status == 200 && bereq.url ~ "now=") {
set beresp.ttl = 60m;
diff --git a/web/nms.gathering.org/nms2/index.html b/web/nms.gathering.org/nms2/index.html
index 42d7a35..141ef5f 100644
--- a/web/nms.gathering.org/nms2/index.html
+++ b/web/nms.gathering.org/nms2/index.html
@@ -21,91 +21,108 @@
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
- <![endif]-->
- <style type="text/css">
- canvas {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- outline: none;
- -webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */
- }
- </style>
+ <![endif]-->
+ <style type="text/css">
+ canvas {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ outline: none;
+ -webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */
+ }
+ </style>
</head>
-<body id="body">
+ <body id="body">
<nav class="navbar navbar-default navbar-static-top">
<div class="container-fluid">
- <div class="navbar-header">
- <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
- <span class="sr-only">Toggle navigation</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a class="navbar-brand" href="#">NMS</a>
- </div>
- <div id="navbar" class="navbar-collapse collapse">
- <ul class="nav navbar-nav">
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Map mode<span class="caret"></span></a>
- <ul class="dropdown-menu" role="menu">
- <li><a href="#ping" onclick="setUpdater(handler_ping)">Ping map</a></li>
- <li><a href="#uplink" onclick="setUpdater(handler_uplinks)">Uplink map</a></li>
- <li><a href="#temp" onclick="setUpdater(handler_temp)">Temperature map</a></li>
- <li><a href="#traffic" onclick="setUpdater(handler_traffic)">Traffic map</a></li>
- <li><a href="#comment" onclick="setUpdater(handler_comment)">Comment spotter</a></li>
- <li><a href="#disco" onclick="setUpdater(handler_disco)">DISCO</a></li>
- <li class="divider"> </li>
- <li><a href="#" onclick="toggleNightMode()" title="Add 'nightMode' anywhere in the url to auto-enable">Toggle Night Mode</a></li>
- <li><a href="#" onclick='showLayer("layerVisibility");'>Set layer visibility</a></li>
- <li class="divider"> </li>
- <li><a href="#" onclick="document.getElementById('nowPickerBox').style.display = 'block';">Travel in time</a></li>
- <li><a href="#" onclick="startReplay();" title="Replay from opening 30 minutes per second">Replay TG</a></li>
- <li><a href="#" onclick="document.getElementById('aboutData').style.display = 'block';">About TG15 data</a></li>
- <li class="divider"> </li>
- <li class="dropdown-header">Map scale</li>
- <li><input type="range" id="scaler" name="points" min="0.2" max="3" step="0.01" onchange="scaleChange()" /></li>
- <li><a href="#">Scale: <div id="scaler-text"></div></a></li>
- <li class="divider"> </li>
- <li><a onclick="document.getElementById('aboutBox').style.display = 'block'; hideSwitch();" style="cursor: pointer;" >About</a></li>
- <li><a onclick="showTimerDebug(); hideSwitch();" style="cursor: pointer;" >Debug timers</a></li>
- </ul>
- </li>
- <li><p id="updater_name" class="navbar-text"></p></li>
- <div class="navbar-form navbar-left">
- <div class="form-group">
- <button class="btn btn-default" id="legend-1"></button>
- <button class="btn btn-default" id="legend-2"></button>
- <button class="btn btn-default" id="legend-3"></button>
- <button class="btn btn-default" id="legend-4"></button>
- <button class="btn btn-default" id="legend-5"></button>
- </div>
- </div>
- </li>
- </ul>
- <ul class="nav navbar-nav navbar-right">
- <li><p id="speed" class="navbar-text" title="Client port speed / Total port speed"></p></li>
- </ul>
- </div><!--/.nav-collapse -->
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" onclick="toggleLayer('aboutBox'); //document.getElementById('aboutBox').style.display = 'block'; hideSwitch();" style="cursor: pointer;" >NMS</a>
+ </div>
+ <div id="navbar" class="navbar-collapse collapse">
+ <ul class="nav navbar-nav">
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Map mode
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu" role="menu">
+ <li><a href="#ping" onclick="setUpdater(handler_ping)">Ping map</a></li>
+ <li><a href="#uplink" onclick="setUpdater(handler_uplinks)">Uplink map</a></li>
+ <li><a href="#temp" onclick="setUpdater(handler_temp)">Temperature map</a></li>
+ <li><a href="#traffic" onclick="setUpdater(handler_traffic)">Traffic map</a></li>
+ <li><a href="#comment" onclick="setUpdater(handler_comment)">Comment spotter</a></li>
+ <li><a href="#disco" onclick="setUpdater(handler_disco)">DISCO</a></li>
+ <li class="divider"> </li>
+ <li><a href="#" onclick="document.getElementById('nowPickerBox').style.display = 'block';">Travel in time</a></li>
+ <li><a href="#" onclick="startReplay();" title="Replay from opening 30 minutes per second">Replay TG</a></li>
+ <li class="divider"> </li>
+ <li><a onclick="showTimerDebug(); hideSwitch();" style="cursor: pointer;" >Debug timers</a></li>
+ </ul>
+ </li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">View<span class="caret"></span></a>
+ <ul class="dropdown-menu" role="menu">
+ <li><a href="#" onclick="toggleNightMode()">Toggle Night Mode</a></li>
+ <li><a href="#" onclick="showBlurBox()">Tweak Night Mode blur</a></li>
+ <li><a href="#" onclick='showLayer("layerVisibility");'>Set layer visibility</a></li>
+ <li class="divider"> </li>
+ <li class="dropdown-header">Map scale</li>
+ <li><a href="#"><label id="scaler-text" for='scaler'></label><input type="range" id="scaler" name="points" min="0.2" max="3" step="0.01" onchange="scaleChange()" /></a></li>
+ </ul>
+ </li>
+ <li><p id="updater_name" class="navbar-text"></p></li>
+ <div class="navbar-form navbar-left">
+ <div class="form-group">
+ <button class="btn btn-default" id="legend-1"></button>
+ <button class="btn btn-default" id="legend-2"></button>
+ <button class="btn btn-default" id="legend-3"></button>
+ <button class="btn btn-default" id="legend-4"></button>
+ <button class="btn btn-default" id="legend-5"></button>
+ </div>
+ </div>
+ </li>
+ </ul>
+ <ul class="nav navbar-nav navbar-right">
+ <li><p id="speed" class="navbar-text" title="Client port speed / Total port speed"></p></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Help
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu" role="menu">
+ <li><a href="#" onclick="toggleLayer('aboutData');">About TG15 data</a></li>
+ <li><a href="#" onclick="toggleLayer('aboutBox');" >About NMS</a></li>
+ <li><a href="#" onclick="toggleLayer('aboutPerformance');" >About Performance</a></li>
+ <li><a href="#" onclick="toggleLayer('aboutKeybindings');" >Keyboard Shortcuts</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div><!--/.nav-collapse -->
</div>
</nav>
<div class="container-fluid">
- <div class="row-fluid">
- <div class="span12">
- <div id="aboutData" class="col-md-4"
- style="position: absolute; display:none; z-index: 130;">
- <div id="abotData" class="panel panel-default">
- <div class="panel-heading"><h3 class="panel-title">About
- the TG15 data
- <button type="button" class="close" aria-label="Close" onclick="document.getElementById('aboutData').style.display = 'none';" style="float: right"><span
- aria-hidden="true">&times;</span></button></h3></div>
- <div class="panel-body">
+ <div class="row-fluid">
+ <div class="span12">
+ <div id="aboutData" class="col-md-4" style="position: absolute; display:none; z-index: 130;">
+ <div id="abotData" class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">About the TG15 data
+ <button type="button" class="close" aria-label="Close" onclick="document.getElementById('aboutData').style.display = 'none';" style="float: right">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </h3>
+ </div>
+ <div class="panel-body">
<p>The data you see from The Gathering 2015 will seem
"broken up". This is not because we don't have data from
the first day, but because the backend was re-written on
@@ -130,147 +147,351 @@
<p>It is also worth mentioning that things like switch
positions are not logged historically, so you see the final
position on the map.</p>
- </div>
-
- </div>
- </div>
- <div id="nowPickerBox" class="panel panel-default" style="position: absolute; display:none; z-index: 130;" >
- <div class="panel-heading"><h3
- class="panel-title">Time travel</h3></div>
- <div class="panel-body">
- <input type="text" class="form-control" placeholder="YYYY-MM-DD hh:mm:ss" id="nowPicker" value="" />
- <button class="btn btn-default" onclick="changeNow();">Travel</button>
- <button class="btn" onclick="document.getElementById('nowPickerBox').style.display = 'none';">Cancel</button>
- </div>
- </div>
- <div style="position: absolute; z-index: 120;" class="col-md-3">
- <div id="info-switch-parent" class="panel panel-default col-d-6" style="display: none; backgroun:silver; position: absolute; z-index: 120;">
- <div class="panel-heading"><h3 class="panel-title"
- id="info-switch-title"></h3></div>
- <div id="info-switch-panel-body">
- <table class="table" id="info-switch-table"></table>
- </div>
- </div>
- </div>
- <div id="aboutBox" class="col-md-4" style="display: none;
- position: absolute; z-index: 100;">
- <div id="abotBox" class="panel panel-default">
- <div class="panel-heading"><h3 class="panel-title">Welcome to NMS
- <button type="button" class="close" aria-labe="Close" onclick="document.getElementById('aboutBox').style.display = 'none';" style="float: right;"><span aria-hidden="true">&times;</span></button></h3></div>
+ </div>
+ </div>
+ </div>
+ <div id="aboutPerformance" class="col-md-4" style="position: absolute; display:none; z-index: 130;">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Performance
+ <button type="button" class="close" aria-label="Close" onclick="document.getElementById('aboutPerformance').style.display = 'none';" style="float: right">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </h3>
+ </div>
+ <table class="table">
+ <tr>
+ <td>Outstanding AJAX requests</td>
+ <td id="outstandingAJAX"></td>
+ </tr>
+ <tr>
+ <td>Overflowed AJAX requests</td>
+ <td id="overflowAJAX"></td>
+ </tr>
+ </table>
+ <div class="panel-body">
+ <p>NMS performance is surprisingly complex. It's split into
+ several parts and dealt with differently.</p>
+ <p>Poller performance is a matter of efficiently collecting
+ data and is mostly handled in the Perl code (and ensuring
+ we use sensible database schemas).</p>
+ <p>Backend performance for the GUI is mostly about not
+ killing the database server. We do NOT try to protect
+ against malicious clients directly, since this is a
+ management system not public-facing, but Varnish is used to
+ cache requests. To be able to do that properly, we need use
+ absolute time when reviewing past events (so "2015-04-02
+ 17:30:00", not "2 hours ago"). We've also tried to minimize
+ the stupidity in the queries. There's still work to be done
+ here, though, as we need to split up a few large backend
+ requests (port-state.pl).</p>
+ <p>Front-end performance is mostly about drawing things
+ sensibly and not completely bombing the memory usage. And
+ about gracefully handling slow backends This will affect
+ you. For example, if you are reviewing past events and the
+ DB is struggling, we'll simply skip a backend request if we
+ have too many outstanding requests, that means you may jump
+ from "17:00" to "18:30" instead of going through
+ "17:30" and "18:00" too. This is working as intended. It
+ also means that you can happily spam the forward/backward
+ keyboard bindings to jump 18 hours forward: You'll overflow
+ the extra AJAX requests for individual requests, but you'll
+ land at the right time when you let go. But there could be
+ a 1 second delay (or more if the backend really struggles)
+ since you'll have to rely on the periodic backend requests
+ instead of the explicit ones triggered on hitting a
+ button.</p>
+ <p>Note that the counters on top are updated on a timer,
+ but this timer is set up at the same time as everything
+ else, which means that it's likely to update at the same
+ time as we fire off AJAX requests, so the 'outstanding ajax
+ requests' counter might either show almost constantly 3 or
+ 0 depending on what timer happens to fire first. This does
+ NOT mean that NMS has 3 requests all the time, just that
+ we're checking right after we fire off AJAX requests every
+ time.</p>
+ <p>NMS also tries to handle drawing OK, which is why things
+ are split into different HTML5 canvases. Blur and text are
+ particularly expensive, but there's no reason to re-paint
+ that all the time, etc).</p>
+ <p>The basic performance experiments are done on TG15 data
+ using a laptop and a VM with 6GB of memory, so it should
+ hold up quite well on "proper" hardware.</p>
+ </div>
+ </div>
+ </div>
+ <div id="aboutKeybindings" class="col-md-4" style="position: absolute; display:none; z-index: 130;">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Keyboard Shortcuts
+ <button type="button" class="close" aria-label="Close" onclick="document.getElementById('aboutKeybindings').style.display = 'none';" style="float: right">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </h3>
+ </div>
+ <table class="table table-condensed">
+ <tr>
+ <th>Key</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td>?</td>
+ <td>Toggle navigation bar</td>
+ </tr>
+ <tr>
+ <td>n</td>
+ <td>Toggle night mode</td>
+ </tr>
+ <tr>
+ <td>1</td>
+ <td>View Ping map</td>
+ </tr>
+ <tr>
+ <td>2</td>
+ <td>View uplink map</td>
+ </tr>
+ <tr>
+ <td>3</td>
+ <td>View temperature map</td>
+ </tr>
+ <tr>
+ <td>4</td>
+ <td>View uplink traffic map</td>
+ </tr>
+ <tr>
+ <td>5</td>
+ <td>View comment spotter map</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ <td>View Disco map</td>
+ </tr>
+ <tr>
+ <td>h</td>
+ <td>Step 1 hour back in time</td>
+ </tr>
+ <tr>
+ <td>j</td>
+ <td>Step 5 minutes back in time</td>
+ </tr>
+ <tr>
+ <td>k</td>
+ <td>Step 5 minutes forward in time</td>
+ </tr>
+ <tr>
+ <td>l</td>
+ <td>Step 1 hour forward in time</td>
+ </tr>
+ <tr>
+ <td>p</td>
+ <td>Toggle playback (1 hour per second)</td>
+ </tr>
+ <tr>
+ <td>r</td>
+ <td>Return to real time</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <div id="nowPickerBox" class="panel panel-default" style="position: absolute; display:none; z-index: 130;" >
+ <div class="panel-heading">
+ <h3 class="panel-title">Time travel</h3>
+ </div>
<div class="panel-body">
-
- <h3>Cool stuff:</h3>
- <ul>
- <li>Click a switch for more info</li>
- <li>Rewind: You can check out state at a specific time or replay from the beginning of the event. Only works for data where we keep time-series (so not for comments)</li>
- <li>Night mode, now with blur. Optional disco-mode (that's
- mainly for testing, though).</li>
- <li>Auto-scaling the viewport/canvas</li>
- <li>Total client speed (up right)</li>
- <li>Generic(-ish) map handlers: provide a name, init-function
- and an update-function and the nms lib does the rest as far as
- integration goes.</li>
- </ul>
- <h3>Todo list front end:</h3>
- <ul>
- <li>Polish time travel UI (Allow playing from a given time at a given speed, play/pause buttons, etc)</li>
- <li>Toggle auto-scale on/off</li>
- <li>Clean up various global variables</li>
- <li>Split blur into separate canvas (canvas is there, it's just
- not used)</li>
- <li>Add DHCP map</li>
- <li>Add magic map (combined map of sorts)</li>
- <li>Adjust updatePorts() frequency based on necessity (1sec
- updates is overkill for regular operation, but needed for time
- travel)</li>
- <li>More info on switches: Port state, possibly link time
- trends</li>
- <li>Moving switches around (like ping.html + edit)</li>
- <li>Split nms.js into multiple components to unclutter the
- code</li>
- <li>Comments: Fix UTF8 garbligash caused by $dbh-&gt;quote()</li>
- <li>More.</li>
- </ul>
- <h3>Todo for backend:</h3>
- <ul>
- <li>IPv6 support</li>
- <li>Close SQL injections (IT'S WIDE OPEN BECAUSE WHY NOT THAT'S NEVER A PROBLEM)</li>
- <li>Split port-state.pl into multiple appropriate pieces. Right
- it mixes heavy time-critical data with less time-critical and
- cheap computation.</li>
- <li>Consider time log of DHCP (right now it just stores the
- most recent timestamp, making time travel impossible)</li>
- <li>Fix SNMP-fetcher so it gets ifXTable and at least ifOperStatus from ifTable. Don't request the entire ifXTable if we can avoid it. Possibly other tweaks.</li>
- <li>Support for adding switches through an API, not just pure SQL.</li>
- <li>Integrate with FAP</li>
- <li>Clean up old interfaces</li>
- <li>Review various agents/tools</li>
- <li>Improve cache headers</li>
- <li>Cache invalidation of comments?</li>
- <li>Re-test the SQL schema. It's been modified and works fine
- on my laptop, but I need to dump it, commit it and test it.</li>
- <li>Munin plugin for ports.</li>
- </ul>
+ <input type="text" class="form-control" placeholder="YYYY-MM-DD hh:mm:ss" id="nowPicker" value="" />
+ <button class="btn btn-default" onclick="changeNow();">Travel</button>
+ <button class="btn" onclick="document.getElementById('nowPickerBox').style.display = 'none';">Cancel</button>
</div>
-
+ </div>
+ <div style="position: absolute; z-index: 120;" class="col-md-4">
+ <div id="info-switch-parent" class="panel panel-default col-d-6" style="display: none; backgroun:silver; position: absolute; z-index: 120;">
+ <div class="panel-heading">
+ <h3 class="panel-title" id="info-switch-title"></h3>
+ </div>
+ <div id="info-switch-panel-body">
+ <table class="table" id="info-switch-table"></table>
+ </div>
</div>
+ </div>
+ <div id="aboutBox" class="col-md-4" style="display: none; position: absolute; z-index: 100;">
+ <div id="abotBox" class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Welcome to NMS
+ <button type="button" class="close" aria-labe="Close" onclick="document.getElementById('aboutBox').style.display = 'none';" style="float: right;">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </h3>
+ </div>
+ <div class="panel-body">
+ <h3>Cool stuff:</h3>
+ <ul>
+ <li>Click a switch for more info</li>
+ <li>Rewind: You can check out state at a specific time or
+ replay from the beginning of the event. Only works for
+ data where we keep time-series (so not for
+ comments)</li>
+ <li>Press '?' to toggle the menu.</li>
+ <li>Auto-scaling the viewport/canvas</li>
+ <li>Total client speed (up right)</li>
+ <li>Generic(-ish) map handlers: provide a name, init-function
+ and an update-function and the nms lib does the rest as far as
+ integration goes.</li>
+ </ul>
+ <h3>Todo list front end:</h3>
+ <ul>
+ <li>Polish time travel UI (Allow playing from a given time at a given speed, play/pause buttons, etc)</li>
+ <li>Better "popup" boxes: It's growing out of control.</li>
+ <li>Toggle auto-scale on/off</li>
+ <li>Clean up various global variables</li>
+ <li>Add DHCP map</li>
+ <li>Adjust updatePorts() frequency based on necessity (1sec
+ updates is overkill for regular operation, but needed for time
+ travel)</li>
+ <li>More info on switches: Port state, possibly link time
+ trends</li>
+ <li>Moving switches around (like ping.html + edit)</li>
+ <li>Split nms.js into multiple components to unclutter the
+ code</li>
+ <li>Comments: Fix UTF8 garbligash caused by $dbh-&gt;quote()</li>
+ <li>Comments: Fix feedback, possibly update the switch-box
+ too, when comments change.</li>
+ </ul>
+ <h3>Todo for backend:</h3>
+ <ul>
+ <li>IPv6 support</li>
+ <li>Provide public API's</li>
+ <li>Close SQL injections (IT'S WIDE OPEN BECAUSE WHY NOT THAT'S NEVER A PROBLEM)</li>
+ <li>Split port-state.pl into multiple appropriate pieces. Right
+ it mixes heavy time-critical data with less time-critical and
+ cheap computation.</li>
+ <li>Rip comments out of port-state.pl completely so it's not
+ bound by the same cache issues and can be reliably
+ refreshed.</li>
+ <li>Consider time log of DHCP (right now it just stores the
+ most recent timestamp, making time travel impossible)</li>
+ <li>Fix SNMP-fetcher so it gets ifXTable and at least
+ ifOperStatus from ifTable. Don't request the entire
+ ifXTable if we can avoid it. Possibly other
+ tweaks.</li>
+ <li>Support for adding switches through an API, not just pure SQL.</li>
+ <li>Integrate with FAP</li>
+ <li>Clean up old interfaces</li>
+ <li>Review various agents/tools</li>
+ <li>Improve cache headers</li>
+ <li>Cache invalidation of comments?</li>
+ <li>Re-test the SQL schema. It's been modified and works fine
+ on my laptop, but I need to dump it, commit it and test it.</li>
+ <li>Munin plugin for ports.</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div id="blurManic" class="panel panel-default" style="display: none; position: absolute; z-index: 100;">
+ <div class="panel-heading">
+ <h1 class="panel-title">Blur tweaks
+ <button type="button" class="close" aria-labe="Close" onclick="document.getElementById('blurManic').style.display = 'none';" style="float: right;">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </h1>
+ </div>
+ <div class="panel-body">
+ <div class="form">
+ <div class="form-group">
+ <label for="shadowBlur">Blur strength</label>
+ <input type="number" id="shadowBlur" class="form-control">
+ </div>
+ <div class="form-group">
+ <label for="shadowColor">Blur color</label>
+ <input type="color" id="shadowColor" class="form-control">
+ </div>
+ <button type="button" class="btn btn-default" onclick="applyBlur();">Apply</button>
+ </div>
</div>
- <div id="debugTimers" class="panel panel-default" style="display: none; position: absolute; z-index: 100;">
- <div class="panel-heading"><h1 class="panel-title">Debug
- timers (e.g.: Break stuff! FAST!)
- <button type="button" class="close" aria-labe="Close" onclick="document.getElementById('debugTimers').style.display = 'none';" style="float: right;"><span aria-hidden="true">&times;</span></button></h1></div>
- <div id="timerTableTop" class="panel-body">
+ </div>
+ </div>
+ <div id="debugTimers" class="panel panel-default" style="display: none; position: absolute; z-index: 100;">
+ <div class="panel-heading">
+ <h1 class="panel-title">Debug timers (e.g.: Break stuff! FAST!)
+ <button type="button" class="close" aria-labe="Close" onclick="document.getElementById('debugTimers').style.display = 'none';" style="float: right;">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </h1>
+ </div>
+ <div id="timerTableTop" class="panel-body">
<p>These are internal timers for the NMS frontend. They are
provided mainly to debug the frontend. Setting AJAX-triggering
counters to ridiculous numbers is not advised (mainly because
it causes server load).</p>
- <table id="timerTable"> </table>
- </div>
- </div>
- <div id="layerVisibility" class="panel panel-default" style="display: none; position: absolute; z-index: 100;">
- <div class="panel-heading"><h1 class="panel-title">Set layer visibility
- <button type="button" class="close" aria-labe="Close" onclick="document.getElementById('layerVisibility').style.display = 'none';" style="float: right;"><span aria-hidden="true">&times;</span></button></h1></div>
- <div class="panel-body">
+ </div>
+ <table id="timerTable"> </table>
+ </div>
+ <div id="layerVisibility" class="panel panel-default" style="display: none; position: absolute; z-index: 100;">
+ <div class="panel-heading">
+ <h1 class="panel-title">Set layer visibility
+ <button type="button" class="close" aria-labe="Close" onclick="document.getElementById('layerVisibility').style.display = 'none';" style="float: right;">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </h1>
+ </div>
+ <div class="panel-body">
<table id="visibilityTable" class="table">
- <tr><td>Background</td><td>
- <button onclick='hideLayer("bgCanvas");'>Hide</button>
- <button onclick='showLayer("bgCanvas");'>Show</button>
- </td></tr>
- <tr><td>Linknets</td><td>
- <button onclick='hideLayer("linkCanvas");'>Hide</button>
- <button onclick='showLayer("linkCanvas");'>Show</button>
- </td></tr>
- <tr><td>Blur</td><td>
- <button onclick='hideLayer("blurCanvas");'>Hide</button>
- <button onclick='showLayer("blurCanvas");'>Show</button>
- </td></tr>
- <tr><td>Switches</td><td>
- <button onclick='hideLayer("switchCanvas");'>Hide</button>
- <button onclick='showLayer("switchCanvas");'>Show</button>
- </td></tr>
- <tr><td>Text</td><td>
- <button onclick='hideLayer("textCanvas");'>Hide</button>
- <button onclick='showLayer("textCanvas");'>Show</button>
- </td></tr>
- <tr><td>Top</td><td>
- <button onclick='hideLayer("topCanvas");'>Hide</button>
- <button onclick='showLayer("topCanvas");'>Show</button>
- </td></tr>
- </table>
- </div>
- </div>
+ <tr>
+ <td>Background</td>
+ <td>
+ <button onclick='hideLayer("bgCanvas");'>Hide</button>
+ <button onclick='showLayer("bgCanvas");'>Show</button>
+ </td>
+ </tr>
+ <tr>
+ <td>Linknets</td>
+ <td>
+ <button onclick='hideLayer("linkCanvas");'>Hide</button>
+ <button onclick='showLayer("linkCanvas");'>Show</button>
+ </td>
+ </tr>
+ <tr>
+ <td>Blur</td>
+ <td>
+ <button onclick='hideLayer("blurCanvas");'>Hide</button>
+ <button onclick='showLayer("blurCanvas");'>Show</button>
+ </td>
+ </tr>
+ <tr>
+ <td>Switches</td>
+ <td>
+ <button onclick='hideLayer("switchCanvas");'>Hide</button>
+ <button onclick='showLayer("switchCanvas");'>Show</button>
+ </td>
+ </tr>
+ <tr>
+ <td>Text</td>
+ <td>
+ <button onclick='hideLayer("textCanvas");'>Hide</button>
+ <button onclick='showLayer("textCanvas");'>Show</button>
+ </td>
+ </tr>
+ <tr>
+ <td>Timestamp</td>
+ <td>
+ <button onclick='hideLayer("topCanvas");'>Hide</button>
+ <button onclick='showLayer("topCanvas");'>Show</button>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
-<canvas id="bgCanvas" width="1920" height="1032" style="position: absolute; z-index: 1;"> </canvas>
-<canvas id="linkCanvas" width="1920" height="1032" style="position: absolute; z-index: 10;"> </canvas>
-<canvas id="blurCanvas" width="1920" height="1032" style="position: absolute; z-index: 20;"> </canvas>
-<canvas id="switchCanvas" width="1920" height="1032" style="position: absolute; z-index: 30;"> </canvas>
-<canvas id="textCanvas" width="1920" height="1032" style="position: absolute; z-index: 40;"> </canvas>
-<canvas id="topCanvas" width="1920" height="1032" style="position: absolute; z-index: 50; cursor: pointer;" onclick="canvasClick(event)">
-</canvas>
+ <canvas id="bgCanvas" width="1920" height="1032" style="position: absolute; z-index: 1;"> </canvas>
+ <canvas id="linkCanvas" width="1920" height="1032" style="position: absolute; z-index: 10;"> </canvas>
+ <canvas id="blurCanvas" width="1920" height="1032" style="position: absolute; z-index: 20;"> </canvas>
+ <canvas id="switchCanvas" width="1920" height="1032" style="position: absolute; z-index: 30;"> </canvas>
+ <canvas id="textCanvas" width="1920" height="1032" style="position: absolute; z-index: 40;"> </canvas>
+ <canvas id="topCanvas" width="1920" height="1032" style="position: absolute; z-index: 50;"> </canvas>
+ <canvas id="inputCanvas" width="1920" height="1032" style="position: absolute; z-index: 60; cursor: pointer;" onclick="canvasClick(event)">
+ </canvas>
-<div style="display:none;"><img id="source" src="img/tg15-salkart-full.png" ></div>
- </div>
- </div>
+ <div style="display:none;"><img id="source" src="img/tg15-salkart-full.png" ></div>
+ </div>
</div><!--/.fluid-container-->
<script src="js/jquery.min.js" type="text/javascript"></script>
<script src="js/bootstrap.min.js" type="text/javascript"></script>
@@ -278,7 +499,7 @@
<script type="text/javascript" src="js/nms-color-util.js"></script>
<script type="text/javascript" src="js/nms-map-handlers.js"></script>
<script type="text/javascript">
- initNMS();
+initNMS();
</script>
-</body>
+ </body>
</html>
diff --git a/web/nms.gathering.org/nms2/js/nms-color-util.js b/web/nms.gathering.org/nms2/js/nms-color-util.js
index 28f7e1b..8ac4ab8 100644
--- a/web/nms.gathering.org/nms2/js/nms-color-util.js
+++ b/web/nms.gathering.org/nms2/js/nms-color-util.js
@@ -1,4 +1,20 @@
+/*
+ * Some stolen colors that look OK.
+ *
+ * PS: Stolen from boostrap, because we use bootstrap and these look good
+ * and match.
+ */
+var lightblue = "#d9edf7";
+var lightgreen = "#dff0d8";
+var lightred = "#f2dede";
+var lightorange = "#fcf8e3";
+var blue = "#337ab7";
+var green = "#5cb85c";
+var teal = "#5bc0de"; // Or whatever the hell that is
+var orange = "#f0ad4e";
+var red = "#d9534f";
+
function gradient_from_latency(latency_ms, latency_secondary_ms)
{
if (latency_secondary_ms === undefined) {
@@ -12,21 +28,21 @@ function gradient_from_latency(latency_ms, latency_secondary_ms)
function rgb_from_latency(latency_ms)
{
if (latency_ms === null || latency_ms === undefined) {
- return '#0000ff';
+ return blue;
}
- var l = latency_ms / 40.0;
+ var l = latency_ms / 50.0;
if (l >= 2.0) {
return 'rgb(255, 0, 0)';
} else if (l >= 1.0) {
l = 2.0 - l;
l = Math.pow(l, 1.0/2.2);
- l = Math.floor(l * 255.0);
+ l = Math.floor(l * 205.0);
return 'rgb(255, ' + l + ', 0)';
} else {
l = Math.pow(l, 1.0/2.2);
l = Math.floor(l * 255.0);
- return 'rgb(' + l + ', 255, 0)';
+ return 'rgb(' + l + ', 205, 0)';
}
}
@@ -42,13 +58,21 @@ function rgb_from_max(x)
return 'rgb(' + Math.floor(colorred) + ", 0, " + Math.floor(colorblue) + ')';
}
+function rgb_from_max2(x)
+{
+ x = x/100;
+ var colorred = 255 * x;
+ var colorgreen = 250 - colorred;
+
+ return 'rgb(' + Math.floor(colorred) + "," + Math.floor(colorgreen) + ', 0 )';
+}
/*
* Return a random-ish color (for testing)
*/
function getRandomColor()
{
- var i = Math.round(Math.random() * 5);
- var colors = [ "white", "red", "pink", "yellow", "orange", "green" ];
+ var colors = [ "white", red, teal, orange, green, blue ];
+ var i = Math.round(Math.random() * (colors.length-1));
return colors[i];
}
diff --git a/web/nms.gathering.org/nms2/js/nms-map-handlers.js b/web/nms.gathering.org/nms2/js/nms-map-handlers.js
index 762788a..f85b06f 100644
--- a/web/nms.gathering.org/nms2/js/nms-map-handlers.js
+++ b/web/nms.gathering.org/nms2/js/nms-map-handlers.js
@@ -53,6 +53,7 @@ var handler_comment = {
init:commentInit,
name:"Fresh comment spotter"
};
+
/*
* Update function for uplink map
* Run periodically when uplink map is active.
@@ -78,15 +79,15 @@ function uplinkUpdater()
}
}
if (uplinks == 0) {
- setSwitchColor(sw,"blue");
+ setSwitchColor(sw,"white");
} else if (uplinks == 1) {
- setSwitchColor(sw,"red");
+ setSwitchColor(sw,red);
} else if (uplinks == 2) {
- setSwitchColor(sw, "yellow");
+ setSwitchColor(sw, orange);
} else if (uplinks == 3) {
- setSwitchColor(sw, "green");
+ setSwitchColor(sw, green);
} else if (uplinks > 3) {
- setSwitchColor(sw, "white");
+ setSwitchColor(sw, blue);
}
}
}
@@ -94,13 +95,26 @@ function uplinkUpdater()
/*
* Init-function for uplink map
*/
+function uplinkInit()
+{
+ setLegend(1,"white","0 uplinks");
+ setLegend(2,red,"1 uplink");
+ setLegend(3,orange,"2 uplinks");
+ setLegend(4,green,"3 uplinks");
+ setLegend(5,blue,"4 uplinks");
+}
+
+/*
+ * Init-function for uplink map
+ */
function trafficInit()
{
- setLegend(1,"blue","0 (N/A)");
- setLegend(5,"red", "1000Mb/s or more");
- setLegend(4,"yellow","100Mb/s to 800Mb/s");
- setLegend(3,"green", "5Mb/s to 100Mb/s");
- setLegend(2,"white","0 to 5Mb/s");
+ var m = 1024 * 1024 / 8;
+ setLegend(1,colorFromSpeed(0),"0 (N/A)");
+ setLegend(5,colorFromSpeed(2000 * m) , "2000Mb/s");
+ setLegend(4,colorFromSpeed(1200 * m),"1200Mb/s");
+ setLegend(3,colorFromSpeed(500 * m),"500Mb/s");
+ setLegend(2,colorFromSpeed(10 * m),"10Mb/s");
}
function trafficUpdater()
@@ -115,37 +129,27 @@ function trafficUpdater()
/ge-0\/0\/46$/.exec(port) ||
/ge-0\/0\/47$/.exec(port))
{
+ if (!nms.switches_then["switches"][sw] ||
+ !nms.switches_then["switches"][sw]["ports"] ||
+ !nms.switches_then["switches"][sw]["ports"][port])
+ continue;
var t = nms.switches_then["switches"][sw]["ports"][port];
var n = nms.switches_now["switches"][sw]["ports"][port];
speed += (parseInt(t["ifhcoutoctets"]) -parseInt(n["ifhcoutoctets"])) / (parseInt(t["time"] - n["time"]));
speed += (parseInt(t["ifhcinoctets"]) -parseInt(n["ifhcinoctets"])) / (parseInt(t["time"] - n["time"]));
}
}
- var m = 1024 * 1024 / 8;
- if (speed == 0) {
- setSwitchColor(sw,"blue");
- } else if (speed > (1000 * m)) {
- setSwitchColor(sw,"red");
- } else if (speed > (800 * m)) {
- setSwitchColor(sw, "yellow");
- } else if (speed > (5 * m)) {
- setSwitchColor(sw, "green");
- } else {
- setSwitchColor(sw, "white");
- }
+ setSwitchColor(sw,colorFromSpeed(speed));
}
}
-/*
- * Init-function for uplink map
- */
-function uplinkInit()
+function colorFromSpeed(speed)
{
- setLegend(1,"blue","0 uplinks");
- setLegend(2,"red","1 uplink");
- setLegend(3,"yellow","2 uplinks");
- setLegend(4,"green","3 uplinks");
- setLegend(5,"white","4 uplinks");
+ var m = 1024 * 1024 / 8;
+ if (speed == 0)
+ return blue;
+ speed = speed < 0 ? 0 : speed;
+ return rgb_from_max2( 100 * (speed / (2 * (1000 * m))));
}
@@ -158,7 +162,7 @@ function temp_color(t)
{
if (t == undefined) {
console.log("Temp_color, but temp is undefined");
- return "blue";
+ return blue;
}
t = parseInt(t) - 20;
t = Math.floor((t / 15) * 100);
@@ -189,13 +193,13 @@ function tempInit()
function pingUpdater()
{
for (var sw in nms.switches_now["switches"]) {
- var c = "blue";
+ var c = blue;
if (nms.ping_data['switches'] && nms.ping_data['switches'][sw])
c = gradient_from_latency(nms.ping_data["switches"][sw]["latency"]);
setSwitchColor(sw, c);
}
for (var ln in nms.switches_now["linknets"]) {
- var c1 = "blue";
+ var c1 = blue;
var c2 = c1;
if (nms.ping_data['linknets'] && nms.ping_data['linknets'][ln]) {
c1 = gradient_from_latency(nms.ping_data["linknets"][ln][0]);
@@ -210,53 +214,54 @@ function pingInit()
setLegend(1,gradient_from_latency(1),"1ms");
setLegend(2,gradient_from_latency(30),"30ms");
setLegend(3,gradient_from_latency(60),"60ms");
- setLegend(4,gradient_from_latency(80),"80ms");
- setLegend(5,"#0000ff" ,"No response");
+ setLegend(4,gradient_from_latency(100),"100ms");
+ setLegend(5,blue ,"No response");
}
function commentUpdater()
{
var realnow = Date.now();
- if (nms.now) {
- realnow = Date.parse(nms.now);
- }
var now = Math.floor(realnow / 1000);
for (var sw in nms.switches_now["switches"]) {
- var c = "green";
+ var c = "white";
var s = nms.switches_now["switches"][sw];
if (s["comments"] && s["comments"].length > 0) {
var then = 0;
+ var active = 0;
+ var persist = 0;
c = "yellow";
for (var v in s["comments"]) {
var then_test = parseInt(s["comments"][v]["time"]);
- if (then_test > then && then_test <= now)
+ if (then_test > then && s["comments"][v]["state"] != "inactive")
then = then_test;
+ if (s["comments"][v]["state"] == "active") {
+ active++;
+ }
+ if (s["comments"][v]["state"] == "persist")
+ persist++;
}
if (then > (now - (60*15))) {
- c = "red";
- } else if (then > (now - (120*60))) {
- c = "orange";
- } else if (then < (now - (60*60*24))) {
- c = "white";
+ c = red;
+ } else if (active > 0) {
+ c = orange;
+ } else if (persist > 0) {
+ c = blue;
+ } else {
+ c = green;
}
- /*
- * Special case during time travel: We have
- * comments, but are not showing them yet.
- */
- if (then == 0)
- c = "green";
}
setSwitchColor(sw, c);
}
}
+
function commentInit()
{
- setLegend(1,"green","0 comments");
- setLegend(2,"white","1d+ old");
- setLegend(3,"red", "0 - 15m old");
- setLegend(4,"orange","15m - 120m old");
- setLegend(5,"yellow" ,"2h - 24h old");
+ setLegend(1,"white","0 comments");
+ setLegend(2,blue,"Persistent comments");
+ setLegend(3,red, "New comments");
+ setLegend(4,orange,"Active comments");
+ setLegend(5,green ,"Old/inactive only");
}
/*
* Testing-function to randomize colors of linknets and switches
@@ -274,10 +279,10 @@ function randomizeColors()
function discoInit()
{
setNightMode(true);
- setLegend(1,"blue","Y");
- setLegend(2,"red", "M");
- setLegend(3,"yellow","C");
- setLegend(4,"green", "A");
+ setLegend(1,blue,"Y");
+ setLegend(2,red, "M");
+ setLegend(3,orange,"C");
+ setLegend(4,green, "A");
setLegend(5,"white","!");
}
diff --git a/web/nms.gathering.org/nms2/js/nms.js b/web/nms.gathering.org/nms2/js/nms.js
index 41b39d6..cf2690e 100644
--- a/web/nms.gathering.org/nms2/js/nms.js
+++ b/web/nms.gathering.org/nms2/js/nms.js
@@ -1,11 +1,14 @@
var nms = {
updater:undefined, // Active updater
+ update_time:0, // Client side timestamp for last update
switches_now:undefined, // Most recent data
switches_then:undefined, // 2 minutes old
speed:0, // Current aggregated speed
ping_data:undefined, // JSON data for ping history.
drawn:false, // Set to 'true' when switches are drawn
switch_showing:"", // Which switch we are displaying (if any).
+ repop_switch:false, // True if we need to repopulate the switch info when port state updates (e.g.: added comments);
+ repop_time:false, // Timestamp in case we get a cached result
nightMode:false,
/*
* Switch-specific variables. These are currently separate from
@@ -13,6 +16,8 @@ var nms = {
* new data.
*/
nightBlur:{}, // Have we blurred this switch or not?
+ shadowBlur:10,
+ shadowColor:"#EEEEEE",
switch_color:{}, // Color for switch
linknet_color:{}, // color for linknet
textDrawn:{}, // Have we drawn text for this switch?
@@ -44,14 +49,40 @@ var nms = {
timers: {
replay:false,
ports:false,
- ping:false,
- map:false,
- speed:false
+ ping:false
},
- deleteComment:0
+ menuShowing:true,
+ /*
+ * This is a list of nms[x] variables that we store in our
+ * settings-cookie when altered and restore on load.
+ */
+ settingsList:[
+ 'shadowBlur',
+ 'shadowColor',
+ 'nightMode',
+ 'menuShowing',
+ 'layerVisibility'
+ ],
+ layerVisibility:{},
+ keyBindings:{
+ '?':toggleMenu,
+ 'n':toggleNightMode,
+ '1':setMapModeFromN,
+ '2':setMapModeFromN,
+ '3':setMapModeFromN,
+ '4':setMapModeFromN,
+ '5':setMapModeFromN,
+ '6':setMapModeFromN,
+ 'h':moveTimeFromKey,
+ 'j':moveTimeFromKey,
+ 'k':moveTimeFromKey,
+ 'l':moveTimeFromKey,
+ 'p':moveTimeFromKey,
+ 'r':moveTimeFromKey,
+ 'default':keyDebug
+ }
};
-
/*
* Returns a handler object.
*
@@ -85,6 +116,7 @@ function nmsTimer(handler, interval, name, description) {
};
}
+
/*
* Drawing primitives.
*
@@ -173,6 +205,9 @@ function initDrawing() {
dr['top'] = {};
dr['top']['c'] = document.getElementById("topCanvas");
dr['top']['ctx'] = dr['top']['c'].getContext('2d');
+ dr['input'] = {};
+ dr['input']['c'] = document.getElementById("inputCanvas");
+ dr['input']['ctx'] = dr['top']['c'].getContext('2d');
}
/*
@@ -196,6 +231,7 @@ function byteCount(bytes) {
function toggleNightMode()
{
setNightMode(!nms.nightMode);
+ saveSettings();
}
/*
@@ -226,7 +262,9 @@ function checkNow(now)
*/
function stringToEpoch(t)
{
- var ret = new Date(Date.parse(t));
+ var foo = t.toString();
+ foo = foo.replace('T',' ');
+ var ret = new Date(Date.parse(foo));
return parseInt(parseInt(ret.valueOf()) / 1000);
}
@@ -266,6 +304,7 @@ function epochToString(t)
*/
function timeReplay()
{
+ replayTime = stringToEpoch(nms.now);
if (replayTime >= tgEnd) {
nms.timers.replay.stop();
return;
@@ -289,7 +328,7 @@ function timeReplay()
function startReplay() {
nms.timers.replay.stop();
resetColors();
- replayTime = tgStart;
+ nms.now = epochToString(tgStart);
timeReplay();
nms.timers.replay.start();;
}
@@ -314,6 +353,21 @@ function changeNow() {
}
}
+function stepTime(n)
+{
+ var now;
+ nms.timers.replay.stop();
+ if (nms.now && nms.now != 0)
+ now = parseInt(stringToEpoch(nms.now));
+ else if (nms.switches_now)
+ now = parseInt(stringToEpoch(/^[^.]*/.exec(nms.switches_now.time)));
+ else
+ return;
+ newtime = parseInt(now) + parseInt(n);
+ nms.now = epochToString(parseInt(newtime));
+ updatePorts();
+}
+
/*
* Hide switch info-box
*/
@@ -335,7 +389,7 @@ function hideSwitch()
/*
* Display info on switch "x" in the info-box
*/
-function switchInfo(x)
+function showSwitch(x)
{
var sw = nms.switches_now["switches"][x];
var swtop = document.getElementById("info-switch-parent");
@@ -345,13 +399,8 @@ function switchInfo(x)
var td1;
var td2;
- if (nms.switch_showing == x) {
- hideSwitch();
- return;
- } else {
- hideSwitch();
- nms.switch_showing = x;
- }
+ hideSwitch();
+ nms.switch_showing = x;
document.getElementById("aboutBox").style.display = "none";
var switchele = document.createElement("table");
switchele.id = "info-switch-table";
@@ -490,16 +539,19 @@ function switchInfo(x)
if (comment["state"] == "active")
col = "danger";
else if (comment["state"] == "inactive")
- col = "active";
+ col = false;
else
col = "info";
tr.className = col;
+ tr.id = "commentRow" + comment["id"];
td1 = tr.insertCell(0);
td2 = tr.insertCell(1);
- var txt = '<div class="btn-group" role="group" aria-label="..."><button type="button" class="btn btn-xs" data-trigger="focus" data-toggle="popover" title="Info" data-content="Comment added ' + epochToString(comment["time"]) + " by user " + comment["username"] + ' and listed as ' + comment["state"] + '">?</button>';
- txt += '<button type="button" class="btn btn-xs" data-trigger="focus" data-toggle="tooltip" title="Mark as deleted" onclick="commentDelete(' + comment["id"] + ');">X</button>';
- txt += '<button type="button" class="btn btn-xs" data-trigger="focus" data-toggle="tooltip" title="Mark as inactive/fixed" onclick="commentInactive(' + comment["id"] + ');">V</button>';
- txt += '<button type="button" class="btn btn-xs" data-trigger="focus" data-toggle="tooltip" title="Mark as persistent" onclick="commentPersist(' + comment["id"] + ');">!</button></div>';
+ td1.style.whiteSpace = "nowrap";
+ td1.style.width = "8em";
+ var txt = '<div class="btn-group" role="group" aria-label="..."><button type="button" class="btn btn-xs btn-default" data-trigger="focus" data-toggle="popover" title="Info" data-content="Comment added ' + epochToString(comment["time"]) + " by user " + comment["username"] + ' and listed as ' + comment["state"] + '"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></button>';
+ txt += '<button type="button" class="btn btn-xs btn-danger" data-trigger="focus" data-toggle="tooltip" title="Mark as deleted" onclick="commentDelete(' + comment["id"] + ');"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>';
+ txt += '<button type="button" class="btn btn-xs btn-success" data-trigger="focus" data-toggle="tooltip" title="Mark as inactive/fixed" onclick="commentInactive(' + comment["id"] + ');"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button>';
+ txt += '<button type="button" class="btn btn-xs btn-info" data-trigger="focus" data-toggle="tooltip" title="Mark as persistent" onclick="commentPersist(' + comment["id"] + ');"><span class="glyphicon glyphicon-star" aria-hidden="true"></span></button></div>';
td1.innerHTML = txt;
td2.textContent = comment['comment'];
}
@@ -514,7 +566,7 @@ function switchInfo(x)
commentbox.id = "commentbox";
commentbox.className = "panel-body";
commentbox.style.width = "100%";
- commentbox.innerHTML = '<div class="form form-inline"><div class="form-group"><input type="text" class="form-control" placeholder="Comment" id="' + x + '-comment"></div><button class="btn btn-default" onclick="addComment(\'' + x + '\',document.getElementById(\'' + x + '-comment\').value); document.getElementById(\'' + x + '-comment\').value = \'added. Wait for it....\';">Add comment</button></div>';
+ commentbox.innerHTML = '<div class="input-group"><input type="text" class="form-control" placeholder="Comment" id="' + x + '-comment"><span class=\"input-group-btn\"><button class="btn btn-default" onclick="addComment(\'' + x + '\',document.getElementById(\'' + x + '-comment\').value); document.getElementById(\'' + x + '-comment\').value = \'\'; document.getElementById(\'' + x + '-comment\').placeholder = \'Comment added. Wait for next refresh.\';">Add comment</button></span></div>';
swtop.appendChild(commentbox);
swtop.style.display = 'block';
}
@@ -530,14 +582,28 @@ function setLegend(x,color,name)
{
var el = document.getElementById("legend-" + x);
el.style.background = color;
- el.innerHTML = name;
+ el.title = name;
+ el.textContent = name;
}
+function updateAjaxInfo()
+{
+ var out = document.getElementById('outstandingAJAX');
+ var of = document.getElementById('overflowAJAX');
+ out.textContent = nms.outstandingAjaxRequests;
+ of.textContent = nms.ajaxOverflow;
+}
/*
* Run periodically to trigger map updates when a handler is active
*/
function updateMap()
{
+ if (!newerSwitches())
+ return;
+ if (!(nms.update_time < (Date.now() - 100) || nms.update_time == 0))
+ return;
+ nms.update_time = Date.now();
+
if (nms.updater != undefined && nms.switches_now && nms.switches_then) {
nms.updater();
}
@@ -579,6 +645,31 @@ function initialUpdate()
}
}
+function resetBlur()
+{
+ nms.nightBlur = {};
+ dr.blur.ctx.clearRect(0,0,canvas.width,canvas.height);
+ drawSwitches();
+}
+
+function applyBlur()
+{
+ var blur = document.getElementById("shadowBlur");
+ var col = document.getElementById("shadowColor");
+ nms.shadowBlur = blur.value;
+ nms.shadowColor = col.value;
+ resetBlur();
+ saveSettings();
+}
+
+function showBlurBox()
+{
+ var blur = document.getElementById("shadowBlur");
+ var col = document.getElementById("shadowColor");
+ blur.value = nms.shadowBlur;
+ col.value = nms.shadowColor;
+ document.getElementById("blurManic").style.display = '';
+}
/*
* Update nms.ping_data
*/
@@ -587,6 +678,7 @@ function updatePing()
var now = nms.now ? ("?now=" + nms.now) : "";
if (nms.outstandingAjaxRequests > 5) {
nms.ajaxOverflow++;
+ updateAjaxInfo();
return;
}
nms.outstandingAjaxRequests++;
@@ -597,50 +689,73 @@ function updatePing()
success: function (data, textStatus, jqXHR) {
nms.ping_data = JSON.parse(data);
initialUpdate();
+ updateMap();
},
complete: function(jqXHR, textStatus) {
nms.outstandingAjaxRequests--;
+ updateAjaxInfo();
}
});
}
-function commentInactive(id) {
+function commentInactive(id)
+{
commentChange(id,"inactive");
}
-function commentPersist(id) {
+function commentPersist(id)
+{
commentChange(id,"persist");
}
-function commentDelete(id) {
- if (id != nms.deleteComment) {
- nms.deleteComment = id;
- alert("Click the button again to delete it");
- return;
+
+function commentDelete(id)
+{
+ var r = confirm("Really delete comment? (Delted comments are still stored in the database, but never displayed)");
+ if (r == true) {
+ commentChange(id,"delete");
}
- commentChange(id,"delete");
}
-function commentChange(id,state) {
+
+/*
+ * FIXME: Neither of these two handle failures in any way, shape or form.
+ * Nor do they really give user-feedback. They work, but only by magic.
+ */
+function commentChange(id,state)
+{
var myData = {
comment:id,
state:state
};
+ var foo = document.getElementById("commentRow" + id);
+ if (foo) {
+ foo.className = '';
+ foo.style.backgroundColor = "silver";
+ }
$.ajax({
type: "POST",
url: "/comment-change.pl",
dataType: "text",
- data:myData
+ data:myData,
+ success: function (data, textStatus, jqXHR) {
+ nms.repop_switch = true;
+ }
});
}
-function addComment(sw,comment) {
+
+function addComment(sw,comment)
+{
var myData = {
switch:sw,
- comment:comment};
- console.log(myData);
+ comment:comment
+ };
$.ajax({
type: "POST",
url: "/switch-comment.pl",
dataType: "text",
- data:myData
+ data:myData,
+ success: function (data, textStatus, jqXHR) {
+ nms.repop_switch = true;
+ }
});
}
/*
@@ -651,6 +766,7 @@ function updatePorts()
var now = "";
if (nms.outstandingAjaxRequests > 5) {
nms.ajaxOverflow++;
+ updateAjaxInfo();
return;
}
nms.outstandingAjaxRequests++;
@@ -665,15 +781,26 @@ function updatePorts()
nms.switches_now = switchdata;
parseIntPlacements();
initialUpdate();
+ updateSpeed();
+ updateMap();
+ if (nms.repop_time == false && nms.repop_switch)
+ nms.repop_time = nms.switches_now.time;
+ else if (nms.repop_switch && nms.switch_showing && nms.repop_time != nms.switches_now.time) {
+ showSwitch(nms.switch_showing,true);
+ nms.repop_switch = false;
+ nms.repop_time = false;
+ }
},
complete: function(jqXHR, textStatus) {
nms.outstandingAjaxRequests--;
+ updateAjaxInfo();
}
});
now="";
if (nms.now != false)
now = "&now=" + nms.now;
nms.outstandingAjaxRequests++;
+ updateAjaxInfo();
$.ajax({
type: "GET",
url: "/port-state.pl?time=5m" + now,
@@ -682,14 +809,32 @@ function updatePorts()
var switchdata = JSON.parse(data);
nms.switches_then = switchdata;
initialUpdate();
+ updateSpeed();
+ updateMap();
},
complete: function(jqXHR, textStatus) {
nms.outstandingAjaxRequests--;
+ updateAjaxInfo();
}
})
}
/*
+ * Returns true if we have now and then-data for switches and that the
+ * "now" is actually newer. Useful for basic sanity and avoiding negative
+ * values when rewinding time.
+ */
+function newerSwitches()
+{
+ if (!nms.switches_now || !nms.switches_then)
+ return false;
+ var now_timestamp = stringToEpoch(nms.switches_now.time);
+ var then_timestamp = stringToEpoch(nms.switches_then.time);
+ if (now_timestamp == 0 || then_timestamp == 0 || then_timestamp >= now_timestamp)
+ return false;
+ return true;
+}
+/*
* Use nms.switches_now and nms.switches_then to update 'nms.speed'.
*
* nms.speed is a total of ifHCInOctets across all client-interfaces
@@ -703,6 +848,7 @@ function updatePorts()
* FIXME: Err, yeah, add this to the tail-end of updatePorts instead :D
*
*/
+
function updateSpeed()
{
var speed_in = parseInt(0);
@@ -710,6 +856,8 @@ function updateSpeed()
var counter=0;
var sw;
var speedele = document.getElementById("speed");
+ if (!newerSwitches())
+ return;
for (sw in nms.switches_now["switches"]) {
for (port in nms.switches_now["switches"][sw]["ports"]) {
if (!nms.switches_now["switches"][sw]["ports"][port]) {
@@ -763,8 +911,8 @@ function updateSpeed()
*/
function drawLinknet(i)
{
- var c1 = nms.linknet_color[i] && nms.linknet_color[i].c1 ? nms.linknet_color[i].c1 : "blue";
- var c2 = nms.linknet_color[i] && nms.linknet_color[i].c2 ? nms.linknet_color[i].c2 : "blue";
+ var c1 = nms.linknet_color[i] && nms.linknet_color[i].c1 ? nms.linknet_color[i].c1 : blue;
+ var c2 = nms.linknet_color[i] && nms.linknet_color[i].c2 ? nms.linknet_color[i].c2 : blue;
if (nms.switches_now.switches[nms.switches_now.linknets[i].sysname1] && nms.switches_now.switches[nms.switches_now.linknets[i].sysname2]) {
connectSwitches(nms.switches_now.linknets[i].sysname1,nms.switches_now.linknets[i].sysname2, c1, c2);
}
@@ -810,16 +958,18 @@ function drawSwitch(sw)
var box = nms.switches_now['switches'][sw]['placement'];
var color = nms.switch_color[sw];
if (color == undefined) {
- color = "blue";
+ color = blue;
}
dr.switch.ctx.fillStyle = color;
+ /*
+ * XXX: This is a left-over from before NMS did separate
+ * canvases, and might be done better elsewhere.
+ */
if (nms.nightMode && nms.nightBlur[sw] != true) {
- dr.switch.ctx.shadowBlur = 10;
- dr.switch.ctx.shadowColor = "#00EE00";
+ dr.blur.ctx.shadowBlur = nms.shadowBlur;
+ dr.blur.ctx.shadowColor = nms.shadowColor;
+ drawBoxBlur(box['x'],box['y'],box['width'],box['height']);
nms.nightBlur[sw] = true;
- } else {
- dr.switch.ctx.shadowBlur = 0;
- dr.switch.ctx.shadowColor = "#000000";
}
drawBox(box['x'],box['y'],box['width'],box['height']);
dr.switch.ctx.shadowBlur = 0;
@@ -871,18 +1021,20 @@ function drawSwitches()
*/
function drawNow()
{
+ if (!nms.switches_now)
+ return;
// XXX: Get rid of microseconds that we get from the backend.
var now = /^[^.]*/.exec(nms.switches_now.time);
dr.top.ctx.font = Math.round(2 * nms.fontSize * canvas.scale) + "px " + nms.fontFace;
dr.top.ctx.clearRect(0,0,Math.floor(800 * canvas.scale),Math.floor(100 * canvas.scale));
dr.top.ctx.fillStyle = "white";
dr.top.ctx.strokeStyle = "black";
- dr.top.ctx.lineWidth = Math.floor(4 * canvas.scale);
+ dr.top.ctx.lineWidth = Math.floor(2 * canvas.scale);
if (dr.top.ctx.lineWidth == 0) {
- dr.top.ctx.lineWidth = Math.round(4 * canvas.scale);
+ dr.top.ctx.lineWidth = Math.round(2 * canvas.scale);
}
- dr.top.ctx.strokeText(now, 0 + margin.text, 30 * canvas.scale);
- dr.top.ctx.fillText(now, 0 + margin.text, 30 * canvas.scale);
+ dr.top.ctx.strokeText(now, 0 + margin.text, 25 * canvas.scale);
+ dr.top.ctx.fillText(now, 0 + margin.text, 25 * canvas.scale);
}
/*
* Draw foreground/scene.
@@ -918,6 +1070,7 @@ function setScale()
nms.textDrawn = {};
drawBG();
drawScene();
+ drawNow();
document.getElementById("scaler").value = canvas.scale;
document.getElementById("scaler-text").innerHTML = (parseFloat(canvas.scale)).toPrecision(3);
@@ -978,7 +1131,10 @@ function scaleChange()
*/
function switchClick(sw)
{
- switchInfo(sw);
+ if (nms.switch_showing == sw)
+ hideSwitch();
+ else
+ showSwitch(sw);
}
/*
@@ -993,11 +1149,11 @@ function resetColors()
return;
if (nms.switches_now.linknets) {
for (var i in nms.switches_now.linknets) {
- setLinknetColors(i, "blue","blue");
+ setLinknetColors(i, blue,blue);
}
}
for (var sw in nms.switches_now.switches) {
- setSwitchColor(sw, "blue");
+ setSwitchColor(sw, blue);
}
}
@@ -1058,6 +1214,14 @@ function setNightMode(toggle) {
nms.nightMode = toggle;
var body = document.getElementById("body");
body.style.background = toggle ? "black" : "white";
+ var nav = document.getElementsByTagName("nav")[0];
+ if (toggle) {
+ dr.blur.c.style.display = '';
+ nav.classList.add('navbar-inverse');
+ } else {
+ dr.blur.c.style.display = 'none';
+ nav.classList.remove('navbar-inverse');
+ }
setScale();
}
/*
@@ -1079,6 +1243,17 @@ function drawBox(x,y,boxw,boxh)
}
/*
+ * Draw the blur for a box.
+ */
+function drawBoxBlur(x,y,boxw,boxh)
+{
+ var myX = Math.floor(x * canvas.scale);
+ var myY = Math.floor(y * canvas.scale);
+ var myX2 = Math.floor((boxw) * canvas.scale);
+ var myY2 = Math.floor((boxh) * canvas.scale);
+ dr.blur.ctx.fillRect(myX,myY, myX2, myY2);
+}
+/*
* Draw text on a box - sideways!
*
* XXX: This is pretty nasty and should also probably take a box as input.
@@ -1149,9 +1324,9 @@ function connectSwitches(insw1, insw2,color1, color2) {
var sw1 = nms.switches_now.switches[insw1].placement;
var sw2 = nms.switches_now.switches[insw2].placement;
if (color1 == undefined)
- color1 = "blue";
+ color1 = blue;
if (color2 == undefined)
- color2 = "blue";
+ color2 = blue;
var x0 = Math.floor((sw1.x + sw1.width/2) * canvas.scale);
var y0 = Math.floor((sw1.y + sw1.height/2) * canvas.scale);
var x1 = Math.floor((sw2.x + sw2.width/2) * canvas.scale);
@@ -1189,14 +1364,10 @@ function initNMS() {
nms.timers.ping = new nmsTimer(updatePing, 1000, "Ping updater", "AJAX request to update ping data");
nms.timers.ping.start();
- nms.timers.map = new nmsTimer(updateMap, 1000, "Map handler", "Updates the map using the chosen map handler (ping, uplink, traffic, etc)");
- nms.timers.map.start();
-
- nms.timers.speed = new nmsTimer(updateSpeed, 1000, "Speed updater", "Recompute total speed (no backend requests)");
- nms.timers.speed.start();
-
nms.timers.replay = new nmsTimer(timeReplay, 1000, "Time machine", "Handler used to change time");
detectHandler();
+ setupKeyhandler();
+ restoreSettings();
}
function detectHandler() {
@@ -1216,9 +1387,6 @@ function detectHandler() {
} else {
setUpdater(handler_ping);
}
- if (/nightMode/.exec(url)) {
- toggleNightMode();
- }
}
/*
@@ -1227,7 +1395,7 @@ function detectHandler() {
* Could probably be cleaned up.
*/
function showTimerDebug() {
- var tableTop = document.getElementById('timerTableTop');
+ var tableTop = document.getElementById('debugTimers');
var table = document.getElementById('timerTable');
var tr, td1, td2;
if (table)
@@ -1238,38 +1406,29 @@ function showTimerDebug() {
table.className = "table";
table.classList.add("table");
table.classList.add("table-default");
- table.border = "1";
- tr = document.createElement("tr");
- td = document.createElement("th");
+ var header = table.createTHead();
+ tr = header.insertRow(0);
+ td = tr.insertCell(0);
td.innerHTML = "Handler";
- tr.appendChild(td);
- td = document.createElement("th");
+ td = tr.insertCell(1);
td.innerHTML = "Interval (ms)";
- tr.appendChild(td);
- td = document.createElement("th");
+ td = tr.insertCell(2);
td.innerHTML = "Name";
- tr.appendChild(td);
- td = document.createElement("th");
+ td = tr.insertCell(3);
td.innerHTML = "Description";
- tr.appendChild(td);
- table.appendChild(tr);
for (var v in nms.timers) {
- console.log(v);
- tr = document.createElement("tr");
- td = document.createElement("td");
- td.innerHTML = nms.timers[v].handle;
- tr.appendChild(td);
- td = document.createElement("td");
- td.innerHTML = "<input type=\"text\" id='handlerValue" + v + "' value='" + nms.timers[v].interval + "'>";
- td.innerHTML += "<button type=\"button\" class=\"btn btn-default\" onclick=\"nms.timers['" + v + "'].setInterval(document.getElementById('handlerValue" + v + "').value);\">Apply</button>";
- tr.appendChild(td);
- td = document.createElement("td");
- td.innerHTML = nms.timers[v].name;
- tr.appendChild(td);
- td = document.createElement("td");
- td.innerHTML = nms.timers[v].description;
- tr.appendChild(td);
- table.appendChild(tr);
+ tr = table.insertRow(-1);
+ td = tr.insertCell(0);
+ td.textContent = nms.timers[v].handle;
+ td = tr.insertCell(1);
+ td.style.width = "15em";
+ var tmp = "<div class=\"input-group\"><input type=\"text\" id='handlerValue" + v + "' value='" + nms.timers[v].interval + "' class=\"form-control\"></input>";
+ tmp += "<span class=\"input-group-btn\"><button type=\"button\" class=\"btn btn-default\" onclick=\"nms.timers['" + v + "'].setInterval(document.getElementById('handlerValue" + v + "').value);\">Apply</button></span></div>";
+ td.innerHTML = tmp;
+ td = tr.insertCell(2);
+ td.textContent = nms.timers[v].name;
+ td = tr.insertCell(3);
+ td.textContent = nms.timers[v].description;
}
tableTop.appendChild(table);
document.getElementById('debugTimers').style.display = 'block';
@@ -1278,9 +1437,169 @@ function showTimerDebug() {
function hideLayer(layer) {
var l = document.getElementById(layer);
l.style.display = "none";
+ if (layer != "layerVisibility")
+ nms.layerVisibility[layer] = false;
+ saveSettings();
}
function showLayer(layer) {
var l = document.getElementById(layer);
l.style.display = "";
+ if (layer != "layerVisibility")
+ nms.layerVisibility[layer] = true;
+ saveSettings();
+}
+
+function toggleLayer(layer) {
+ var l = document.getElementById(layer);
+ if (l.style.display == 'none')
+ l.style.display = '';
+ else
+ l.style.display = 'none';
+}
+
+function applyLayerVis()
+{
+ for (var l in nms.layerVisibility) {
+ var layer = document.getElementById(l);
+ if (layer)
+ layer.style.display = nms.layerVisibility[l] ? '' : 'none';
+ }
+}
+
+function setMenu()
+{
+ var nav = document.getElementsByTagName("nav")[0];
+ nav.style.display = nms.menuShowing ? '' : 'none';
+ resizeEvent();
+
+}
+function toggleMenu()
+{
+ nms.menuShowing = ! nms.menuShowing;
+ setMenu();
+ saveSettings();
+}
+
+function setMapModeFromN(e,key)
+{
+ switch(key) {
+ case '1':
+ setUpdater(handler_ping);
+ break;
+ case '2':
+ setUpdater(handler_uplinks);
+ break;
+ case '3':
+ setUpdater(handler_temp);
+ break;
+ case '4':
+ setUpdater(handler_traffic);
+ break;
+ case '5':
+ setUpdater(handler_comment);
+ break;
+ case '6':
+ setUpdater(handler_disco);
+ break;
+ }
+ return true;
+}
+
+function moveTimeFromKey(e,key)
+{
+ switch(key) {
+ case 'h':
+ stepTime(-3600);
+ break;
+ case 'j':
+ stepTime(-300);
+ break;
+ case 'k':
+ stepTime(300);
+ break;
+ case 'l':
+ stepTime(3600);
+ break;
+ case 'p':
+ if(nms.timers.replay.handle)
+ nms.timers.replay.stop();
+ else {
+ timeReplay();
+ nms.timers.replay.start();
+ }
+ break;
+ case 'r':
+ nms.timers.replay.stop();
+ nms.now = false;
+ updatePorts();
+ break;
+ }
+ return true;
+}
+
+var debugEvent;
+function keyDebug(e)
+{
+ console.log(e);
+ debugEvent = e;
+}
+
+function keyPressed(e)
+{
+ if (e.target.nodeName == "INPUT") {
+ return false;
+ }
+ var key = String.fromCharCode(e.keyCode);
+ if (nms.keyBindings[key])
+ return nms.keyBindings[key](e,key);
+ if (nms.keyBindings['default'])
+ return nms.keyBindings['default'](e,key);
+ return false;
+}
+
+function setupKeyhandler()
+{
+ var b = document.getElementsByTagName("body")[0];
+ b.onkeypress = function(e){keyPressed(e);};
+}
+
+
+function getCookie(cname) {
+ var name = cname + "=";
+ var ca = document.cookie.split(';');
+ for(var i=0; i<ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0)==' ')
+ c = c.substring(1);
+ if (c.indexOf(name) == 0)
+ return c.substring(name.length,c.length);
+ }
+ return "";
+}
+
+function saveSettings()
+{
+ var foo={};
+ for (var v in nms.settingsList) {
+ foo[nms.settingsList[v]] = nms[nms.settingsList[v]];
+ }
+ document.cookie = 'nms='+btoa(JSON.stringify(foo));
+}
+
+function restoreSettings()
+{
+ var retrieve = JSON.parse(atob(getCookie("nms")));
+ for (var v in retrieve) {
+ nms[v] = retrieve[v];
+ }
+ setScale();
+ setMenu();
+ setNightMode(nms.nightMode);
+ applyLayerVis();
+}
+
+function forgetSettings()
+{
+ document.cookie = 'nms=' + btoa('{}');
}
diff --git a/web/nms.gathering.org/port-state.pl b/web/nms.gathering.org/port-state.pl
index b21bd8d..9df31c0 100755
--- a/web/nms.gathering.org/port-state.pl
+++ b/web/nms.gathering.org/port-state.pl
@@ -69,7 +69,12 @@ while (my $ref = $q4->fetchrow_hashref()) {
# push @{$json{'linknets'}}, $ref;
}
-my $q5 = $dbh->prepare ('select ' . $now . ' as time;');
+my $q5;
+if (defined($cin)) {
+ $q5 = $dbh->prepare ('select (' . $now . ' - \'' . $cin . '\'::interval) as time;');
+} else {
+ $q5 = $dbh->prepare ('select ' . $now . ' as time;');
+}
$q5->execute();
$json{'time'} = $q5->fetchrow_hashref()->{'time'};
diff --git a/web/nms.gathering.org/uplinkkart-text.pl b/web/nms.gathering.org/uplinkkart-text.pl
index de91e92..c4b31a9 100755
--- a/web/nms.gathering.org/uplinkkart-text.pl
+++ b/web/nms.gathering.org/uplinkkart-text.pl
@@ -17,7 +17,7 @@ print <<"EOF";
<map name="switches">
EOF
-my $q = $dbh->prepare("SELECT * FROM switches NATURAL JOIN placements WHERE switchtype = 'dlink3100'");
+my $q = $dbh->prepare("SELECT * FROM switches NATURAL JOIN placements WHERE switchtype = 'ex2200'");
$q->execute();
while (my $ref = $q->fetchrow_hashref()) {
$ref->{'placement'} =~ /\((\d+),(\d+)\),\((\d+),(\d+)\)/;