dark mode and websockets
[kismet-logviewer.git] / logviewer / static / js / kismet.ui.base.js
1 (
2   typeof define === "function" ? function (m) { define("kismet-ui-base-js", m); } :
3   typeof exports === "object" ? function (m) { module.exports = m(); } :
4   function(m){ this.kismet_ui_base = m(); }
5 )(function () {
6
7 "use strict";
8
9 var exports = {};
10
11 var local_uri_prefix = "";
12 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
13     local_uri_prefix = KISMET_URI_PREFIX;
14
15 // Flag we're still loading
16 exports.load_complete = 0;
17
18 /* Fetch the system user */
19 $.get(local_uri_prefix + "system/user_status.json")
20 .done(function(data) {
21     exports.system_user = data['kismet.system.user'];
22 })
23 .fail(function() {
24     exports.system_user = "[unknown]";
25 });
26
27 // Load our css
28 $('<link>')
29     .appendTo('head')
30     .attr({
31         type: 'text/css',
32         rel: 'stylesheet',
33         href: local_uri_prefix + 'css/kismet.ui.base.css'
34     });
35
36 /* Call from index to set the required ajax parameters universally */
37 exports.ConfigureAjax = function() {
38     $.ajaxSetup({
39         beforeSend: function (xhr) {
40             var user = kismet.getStorage('kismet.base.login.username', 'kismet');
41             var pw =  kismet.getStorage('kismet.base.login.password', '');
42
43             xhr.setRequestHeader ("Authorization", "Basic " + btoa(user + ":" + pw));
44         },
45
46         /*
47         dataFilter: function(data, type) {
48             try {
49                 var json = JSON.parse(data);
50                 var sjson = kismet.sanitizeObject(json);
51                 console.log(JSON.stringify(sjson));
52                 return JSON.stringify(sjson);
53             } catch {
54                 return data;
55             }
56         },
57         */
58     });
59 }
60
61 exports.ConfigureAjax();
62
63 var eventbus_ws_listeners = [];
64
65 exports.eventbus_ws = null;
66
67 exports.SubscribeEventbus = function(topic, fields, callback) {
68     var sub = {
69         "topic": topic,
70         "callback": callback
71     }
72
73     if (fields.length > 0)
74         sub["fields"] = fields;
75
76     eventbus_ws_listeners.push(sub);
77
78     if (exports.eventbus_ws != null && exports.eventbus_ws.readyState == 1) {
79         var sub_req = {
80             "SUBSCRIBE": sub["topic"],
81         };
82
83         if ("fields" in sub)
84             sub_req["fields"] = sub["fields"]
85
86         exports.eventbus_ws.send(JSON.stringify(sub_req));
87     }
88 }
89
90 exports.OpenEventbusWs = function() {
91     var proto = "";
92
93     if (document.location.protocol == "https:")
94         proto = "wss"
95     else
96         proto = "ws"
97
98     var user = kismet.getStorage('kismet.base.login.username', 'kismet');
99     var pw =  kismet.getStorage('kismet.base.login.password', '');
100
101     var host = new URL(document.URL);
102
103     var ws_url = `${proto}://${host.host}/${KISMET_PROXY_PREFIX}eventbus/events.ws?user=${encodeURIComponent(user)}&password=${encodeURIComponent(pw)}`
104
105     exports.eventbus_ws = new WebSocket(ws_url);
106     
107     exports.eventbus_ws.onclose = function(event) {
108         console.log("eventbus ws closed");
109
110         setTimeout(function() { exports.OpenEventbusWs(); }, 500);
111     };
112
113     exports.eventbus_ws.onmessage = function(event) {
114         try {
115             var json = JSON.parse(event.data);
116
117             for (var x in json) {
118                 for (var sub of eventbus_ws_listeners) {
119                     if (sub["topic"] === x) {
120                         sub["callback"](json[x]);
121                     }
122                 }
123             }
124         } catch (e) {
125             console.log(e);
126         }
127     }
128
129     exports.eventbus_ws.onopen = function(event) {
130         for (var sub of eventbus_ws_listeners) {
131             var sub_req = {
132                 "SUBSCRIBE": sub["topic"],
133             };
134
135             if ("fields" in sub)
136                 sub_req["fields"] = sub["fields"]
137
138             exports.eventbus_ws.send(JSON.stringify(sub_req));
139         }
140     }
141 };
142
143 exports.SubscribeEventbus("TIMESTAMP", [], function(data) {
144     data = kismet.sanitizeObject(data);
145     kismet.timestamp_sec = data['kismet.system.timestamp.sec'];
146     kismet.timestamp_usec = data['kismet.system.timestamp.usec'];
147 });
148
149 // exports.SubscribeEventbus("MESSAGE", [], function(e) { console.log(e); });
150
151 /* Define some callback functions for the table */
152
153 exports.renderLastTime = function(data, type, row, meta) {
154     return (new Date(data * 1000).toString()).substring(4, 25);
155 }
156
157 exports.renderDataSize = function(data, type, row, meta) {
158     if (type === 'display')
159         return kismet.HumanReadableSize(data);
160
161     return data;
162 }
163
164 exports.renderMac = function(data, type, row, meta) {
165     if (typeof(data) === 'undefined') {
166         return "<i>n/a</i>";
167     }
168
169     return kismet.censorMAC(data);
170 }
171
172 exports.renderSignal = function(data, type, row, meta) {
173     if (data == 0)
174         return "<i>n/a</i>"
175     return data;
176 }
177
178 exports.renderChannel = function(data, type, row, meta) {
179     if (data == 0)
180         return "<i>n/a</i>"
181     return data;
182 }
183
184 exports.renderPackets = function(data, type, row, meta) {
185     return "<i>Preparing graph</i>";
186 }
187
188 exports.renderUsecTime = function(data, type, row, meta) {
189     if (data == 0)
190         return "<i>n/a</i>";
191
192     var data_sec = data / 1000000;
193
194     var days = Math.floor(data_sec / 86400);
195     var hours = Math.floor((data_sec / 3600) % 24);
196     var minutes = Math.floor((data_sec / 60) % 60);
197     var seconds = Math.floor(data_sec % 60);
198
199     var ret = "";
200
201     if (days > 0)
202         ret = ret + days + "d ";
203     if (hours > 0 || days > 0)
204         ret = ret + hours + "h ";
205     if (minutes > 0 || hours > 0 || days > 0)
206         ret = ret + minutes + "m ";
207     ret = ret + seconds + "s";
208
209     return ret;
210 }
211
212 exports.drawPackets = function(dyncolumn, table, row) {
213     // Find the column
214     var rid = table.column(dyncolumn.name + ':name').index();
215     var match = "td:eq(" + rid + ")";
216
217     var data = row.data();
218
219     // Simplify the RRD so that the bars are thicker in the graph, which
220     // I think looks better.  We do this with a transform function on the
221     // RRD function, and we take the peak value of each triplet of samples
222     // because it seems to be more stable, visually
223     //
224     // We use the aliased field names we extracted from just the minute
225     // component of the per-device packet RRD
226     var simple_rrd =
227         kismet.RecalcRrdData2(data, kismet.RRD_SECOND,
228             {
229                 transform: function(data, opt) {
230                     var slices = 3;
231                     var peak = 0;
232                     var ret = new Array();
233
234                     for (var ri = 0; ri < data.length; ri++) {
235                         peak = Math.max(peak, data[ri]);
236
237                         if ((ri % slices) == (slices - 1)) {
238                             ret.push(peak);
239                             peak = 0;
240                         }
241                     }
242
243                     return ret;
244                 }
245             });
246
247     // Render the sparkline
248     $(match, row.node()).sparkline(simple_rrd,
249         { type: "bar",
250             width: "100px",
251             height: 12,
252             barColor: kismet_theme.sparkline_main,
253             nullColor: kismet_theme.sparkline_main,
254             zeroColor: kismet_theme.sparkline_main,
255         });
256 }
257
258 // Define the basic columns
259 kismet_ui.AddDeviceColumn('column_name', {
260     sTitle: 'Name',
261     field: 'kismet.device.base.commonname',
262     description: 'Device name',
263     width: "150px",
264     renderfunc: function(d, t, r, m) {
265         d = kismet.censorMAC(d);
266         return kismet.censorString(d);
267         // return kismet.censorMAC(d);
268         /*
269         var dname = kismet.censorMAC(d);
270         return (dname.length > 24) ? dname.substr(0, 23) + '&hellip;' : dname;
271         */
272     }
273 });
274
275 kismet_ui.AddDeviceColumn('column_type', {
276     sTitle: 'Type',
277     field: 'kismet.device.base.type',
278     description: 'Device type',
279     width: '75px',
280 });
281
282 kismet_ui.AddDeviceColumn('column_phy', {
283     sTitle: 'Phy',
284     field: 'kismet.device.base.phyname',
285     description: 'Capture Phy name',
286     width: "75px",
287 });
288
289 kismet_ui.AddDeviceColumn('column_crypto', {
290     sTitle: 'Crypto',
291     field: 'kismet.device.base.crypt',
292     description: 'Encryption',
293     width: "75px",
294     renderfunc: function(d, t, r, m) {
295         if (d == "") {
296             return "n/a";
297         }
298
299         return d;
300     },
301 });
302
303 kismet_ui.AddDeviceColumn('column_signal', {
304     sTitle: 'Sgn',
305     field: 'kismet.device.base.signal/kismet.common.signal.last_signal',
306     description: 'Last-seen signal',
307     width: "30px",
308     sClass: "dt-body-right",
309     renderfunc: function(d, t, r, m) {
310         return exports.renderSignal(d, t, r, m);
311     },
312 });
313
314 kismet_ui.AddDeviceColumn('column_channel', {
315     sTitle: 'Chan',
316     field: 'kismet.device.base.channel',
317     description: 'Last-seen channel',
318     width: "40px",
319     sClass: "dt-body-right",
320     renderfunc: function(d, t, r, m) {
321         if (d != 0) {
322             return d;
323         } else if ('kismet.device.base.frequency' in r &&
324             r['kismet.device.base_frequency'] != 0) {
325                 return kismet_ui.GetPhyConvertedChannel(r['kismet.device.base.phyname'], r['kismet.device.base.frequency']);
326         } else {
327             return "<i>n/a</i>";
328         }
329     },
330 });
331
332 kismet_ui.AddDeviceColumn('column_time', {
333     sTitle: 'Last Seen',
334     field: 'kismet.device.base.last_time',
335     description: 'Last-seen time',
336     renderfunc: function(d, t, r, m) {
337         return exports.renderLastTime(d, t, r, m);
338     },
339     searchable: true,
340     visible: false,
341     orderable: true,
342     width: "100px",
343 });
344
345 kismet_ui.AddDeviceColumn('column_first_time', {
346     sTitle: 'First Seen',
347     field: 'kismet.device.base.first_time',
348     description: 'First-seen time',
349     renderfunc: function(d, t, r, m) {
350         return exports.renderLastTime(d, t, r, m);
351     },
352     searchable: true,
353     visible: false,
354     orderable: true,
355     width: "100px",
356 });
357
358 kismet_ui.AddDeviceColumn('column_datasize', {
359     sTitle: 'Data',
360     field: 'kismet.device.base.datasize',
361     description: 'Data seen',
362     bUseRendered: false,
363     sClass: "dt-body-right",
364     width: "40px",
365     renderfunc: function(d, t, r, m) {
366         return exports.renderDataSize(d, t, r, m);
367     },
368 });
369
370 // Fetch just the last time field, we use the hidden rrd_min_data field to assemble
371 // the rrd.  This is a hack to be more efficient and not send the house or day
372 // rrd records along with it.
373 kismet_ui.AddDeviceColumn('column_packet_rrd', {
374     sTitle: 'Packets',
375     field: ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'packet.rrd.last_time'],
376     name: 'packets',
377     width: "110px",
378     description: 'Packet history graph',
379     renderfunc: function(d, t, r, m) {
380         return exports.renderPackets(d, t, r, m);
381     },
382     drawfunc: function(d, t, r) {
383         return exports.drawPackets(d, t, r);
384     },
385     orderable: false,
386     searchable: false,
387 });
388
389 // Hidden col for packet minute rrd data
390 // We MUST define ONE FIELD and then multiple additional fields are permitted
391 kismet_ui.AddDeviceColumn('column_rrd_minute_hidden', {
392     sTitle: 'packets_rrd_min_data',
393     field: 
394         ['kismet.device.base.packets.rrd/kismet.common.rrd.serial_time', 'kismet.common.rrd.serial_time'],
395     fields: [
396         ['kismet.device.base.packets.rrd/kismet.common.rrd.minute_vec', 'kismet.common.rrd.minute_vec'],
397         ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'kismet.common.rrd.last_time'],
398     ],
399     name: 'packets_rrd_min_data',
400     searchable: false,
401     visible: false,
402     selectable: false,
403     orderable: false
404 });
405
406 // Hidden col for key, mappable, we need to be sure to
407 // fetch it so we can use it as an index
408 kismet_ui.AddDeviceColumn('column_device_key_hidden', {
409     sTitle: 'Key',
410     field: 'kismet.device.base.key',
411     searchable: false,
412     orderable: false,
413     visible: false,
414     selectable: false,
415 });
416
417 // HIdden for phy to always turn it on
418 kismet_ui.AddDeviceColumn('column_phy_hidden', {
419     sTitle: 'Phy',
420     field: 'kismet.device.base.phyname',
421     searchable: true,
422     visible: false,
423     orderable: false,
424     selectable: false,
425 });
426
427 // Hidden col for mac address, searchable
428 kismet_ui.AddDeviceColumn('column_device_mac_hidden', {
429     sTitle: 'MAC',
430     field: 'kismet.device.base.macaddr',
431     searchable: true,
432     orderable: false,
433     visible: false,
434     selectable: false,
435 });
436
437 // Hidden col for mac address, searchable
438 kismet_ui.AddDeviceColumn('column_device_mac', {
439     sTitle: 'MAC',
440     field: 'kismet.device.base.macaddr',
441     description: 'MAC address',
442     searchable: true,
443     orderable: true,
444     visible: false,
445     width: "70px",
446     renderfunc: function(d, t, r, m) {
447         return exports.renderMac(d, t, r, m);
448     },
449 });
450
451 // Hidden column for computing freq in the absence of channel
452 kismet_ui.AddDeviceColumn('column_frequency_hidden', {
453     sTitle: 'Frequency',
454     field: 'kismet.device.base.frequency',
455     searchable: false,
456     visible: false,
457     orderable: false,
458     selectable: false,
459 });
460
461 kismet_ui.AddDeviceColumn('column_frequency', {
462     sTitle: 'Frequency',
463     field: 'kismet.device.base.frequency',
464     description: 'Frequency',
465     name: 'frequency',
466     searchable: false,
467     visible: false,
468     orderable: true,
469 });
470
471 // Manufacturer name
472 kismet_ui.AddDeviceColumn('column_manuf', {
473     sTitle: 'Manuf',
474     field: 'kismet.device.base.manuf',
475     description: 'Manufacturer',
476     name: 'manuf',
477     searchable: true,
478     visible: false,
479     orderable: true,
480     width: "70px",
481     renderfunc: function(d, t, r, m) {
482         return (d.length > 32) ? d.substr(0, 31) + '&hellip;' : d;
483     }
484 });
485
486
487 // Add the (quite complex) device details.
488 // It has a priority of -1000 because we want it to always come first.
489 //
490 // There is no filter function because we always have base device
491 // details
492 //
493 // There is no render function because we immediately fill it during draw.
494 //
495 // The draw function will populate the kismet devicedata when pinged
496 kismet_ui.AddDeviceDetail("base", "Device Info", -1000, {
497     draw: function(data, target, options, storage) {
498         target.devicedata(data, {
499             "id": "genericDeviceData",
500             "fields": [
501             {
502                 field: "kismet.device.base.name",
503                 title: "Name",
504                 help: "Device name, derived from device characteristics or set as a custom name by the user.",
505                 draw: function(opts) {
506                     var name = opts['data']['kismet.device.base.username'];
507
508                     if (typeof(name) != 'undefined' && name != "") { 
509                         name = kismet.censorString(name);
510                     }
511                     
512                     if (typeof(name) == 'undefined' || name == "") { 
513                         name = opts['data']['kismet.device.base.commonname'];
514                         name = kismet.censorString(name);
515                     }
516
517                     if (typeof(name) == 'undefined' || name == "") {
518                         name = opts['data']['kismet.device.base.macaddr'];
519                         name = kismet.censorMAC(name);
520                     }
521
522
523                     var nameobj = 
524                         $('<a>', {
525                             'href': '#'
526                         })
527                         .html(name);
528
529                     nameobj.editable({
530                         type: 'text',
531                         mode: 'inline',
532                         success: function(response, newvalue) {
533                             var jscmd = {
534                                 "username": newvalue
535                             };
536                             var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
537                             $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_name.cmd", postdata, "json");
538                         }
539                     });
540
541                     var container =
542                         $('<span>');
543                     container.append(nameobj);
544                     container.append(
545                         $('<i>', {
546                             'class': 'copyuri pseudolink fa fa-copy',
547                             'style': 'padding-left: 5px;',
548                             'data-clipboard-text': `${name}`, 
549                         })
550                     );
551
552                     return container;
553                 }
554             },
555
556             {
557                 field: "kismet.device.base.tags/notes",
558                 title: "Notes",
559                 help: "Abritrary notes",
560                 draw: function(opts) {
561                     var notes = "";
562
563                     if ('kismet.device.base.tags' in opts['data'])
564                         notes = opts['data']['kismet.device.base.tags']['notes'];
565
566                     if (notes == null)
567                         notes = "";
568                     
569                     var notesobj = 
570                         $('<a>', {
571                             'href': '#',
572                             'data-type': 'textarea',
573                         })
574                         .html(notes.convertNewlines());
575
576                     notesobj.editable({
577                         type: 'text',
578                         mode: 'inline',
579                         success: function(response, newvalue) {
580                             var jscmd = {
581                                 "tagname": "notes",
582                                 "tagvalue": newvalue.escapeSpecialChars(),
583                             };
584                             var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
585                             $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_tag.cmd", postdata, "json");
586                         }
587                     });
588
589                     return notesobj;
590                 }
591             },
592
593
594             {
595                 field: "kismet.device.base.macaddr",
596                 title: "MAC Address",
597                 help: "Unique per-phy address of the transmitting device, when available.  Not all phy types provide MAC addresses, however most do.",
598                 draw: function(opts) {
599                     var mac = kismet.censorMAC(opts['value']);
600
601                     var container =
602                         $('<span>');
603                     container.append(
604                         $('<span>').html(mac)
605                     );
606                     container.append(
607                         $('<i>', {
608                             'class': 'copyuri pseudolink fa fa-copy',
609                             'style': 'padding-left: 5px;',
610                             'data-clipboard-text': `${mac}`, 
611                         })
612                     );
613
614                     return container;
615                 }
616             },
617             {
618                 field: "kismet.device.base.manuf",
619                 title: "Manufacturer",
620                 empty: "<i>Unknown</i>",
621                 help: "Manufacturer of the device, derived from the MAC address.  Manufacturers are registered with the IEEE and resolved in the files specified in kismet.conf under 'manuf='",
622             },
623             {
624                 field: "kismet.device.base.type",
625                 liveupdate: true,
626                 title: "Type",
627                 empty: "<i>Unknown</i>"
628             },
629             {
630                 field: "kismet.device.base.first_time",
631                 liveupdate: true,
632                 title: "First Seen",
633                 draw: function(opts) {
634                     return new Date(opts['value'] * 1000);
635                 }
636             },
637             {
638                 field: "kismet.device.base.last_time",
639                 liveupdate: true,
640                 title: "Last Seen",
641                 draw: function(opts) {
642                     return new Date(opts['value'] * 1000);
643                 }
644             },
645             {
646                 field: "group_frequency",
647                 groupTitle: "Frequencies",
648                 id: "group_frequency",
649                 liveupdate: true,
650
651                 fields: [
652                 {
653                     field: "kismet.device.base.channel",
654                     title: "Channel",
655                     empty: "<i>None Advertised</i>",
656                     help: "The phy-specific channel of the device, if known.  The advertised channel defines a specific, known channel, which is not affected by channel overlap.  Not all phy types advertise fixed channels, and not all device types have fixed channels.  If an advertised channel is not available, the primary frequency is used.",
657                 },
658                 {
659                     field: "kismet.device.base.frequency",
660                     title: "Main Frequency",
661                     help: "The primary frequency of the device, if known.  Not all phy types advertise a fixed frequency in packets.",
662                     draw: function(opts) {
663                         return kismet.HumanReadableFrequency(opts['value']);
664                     },
665                     filterOnZero: true,
666                 },
667                 {
668                     field: "frequency_map",
669                     span: true,
670                     liveupdate: true,
671                     filter: function(opts) {
672                         try {
673                             return (Object.keys(opts['data']['kismet.device.base.freq_khz_map']).length >= 1);
674                         } catch (error) {
675                             return 0;
676                         }
677                     },
678                     render: function(opts) {
679                         var d = 
680                             $('<div>', {
681                                 style: 'width: 80%; height: 250px',
682                             })
683                             .append(
684                                 $('<canvas>', {
685                                     id: 'freqdist',
686                                 })
687                             );
688
689                         return d;
690
691                     },
692                     draw: function(opts) {
693                         var legend = new Array();
694                         var data = new Array();
695
696                         for (var fk in opts['data']['kismet.device.base.freq_khz_map']) {
697                             legend.push(kismet.HumanReadableFrequency(parseInt(fk)));
698                             data.push(opts['data']['kismet.device.base.freq_khz_map'][fk]);
699                         }
700
701                         var barChartData = {
702                             labels: legend,
703
704                             datasets: [{
705                                 label: 'Dataset 1',
706                                 backgroundColor: 'rgba(46, 99, 162, 1)',
707                                 borderWidth: 0,
708                                 data: data,
709                             }]
710
711                         };
712
713                         if ('freqchart' in window[storage]) {
714                             window[storage].freqchart.data.labels = legend;
715                             window[storage].freqchart.data.datasets[0].data = data;
716                             window[storage].freqchart.update();
717                         } else {
718                             window[storage].freqchart = 
719                                 new Chart($('canvas', opts['container']), {
720                                     type: 'bar',
721                                     data: barChartData,
722                                     options: {
723                                         maintainAspectRatio: false,
724                                         animation: false,
725                                         plugins: { 
726                                             legend: {
727                                                 display: false,
728                                             },
729                                             title: {
730                                                 display: true,
731                                                 text: 'Packet frequency distribution'
732                                             }
733                                         }
734                                     }
735                                 });
736
737                             window[storage].freqchart.update();
738                         }
739                     }
740                 },
741                 ]
742             },
743             {
744                 field: "group_signal_data",
745                 groupTitle: "Signal",
746                 id: "group_signal_data",
747
748                 filter: function(opts) {
749                     var db = kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.last_signal");
750
751                     if (db == 0)
752                         return false;
753
754                     return true;
755                 },
756
757                 fields: [
758                 {
759                     field: "kismet.device.base.signal/kismet.common.signal.signal_rrd",
760                     filterOnZero: true,
761                     title: "Monitor Signal",
762
763                     render: function(opts) {
764                         return '<div class="monitor pseudolink">Monitor</div>';
765                     },
766                     draw: function(opts) {
767                         $('div.monitor', opts['container'])
768                         .on('click', function() {
769                             exports.DeviceSignalDetails(opts['data']['kismet.device.base.key']);
770                         });
771                     },
772
773                     /* RRD - come back to this later
774                     render: function(opts) {
775                         return '<div class="rrd" id="' + opts['key'] + '" />';
776                     },
777                     draw: function(opts) {
778                         var rrdiv = $('div', opts['container']);
779
780                         var rrdata = kismet.RecalcRrdData(opts['data']['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'], last_devicelist_time, kismet.RRD_MINUTE, opts['data']['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.minute_vec'], {});
781
782                         // We assume the 'best' a signal can usefully be is -20dbm,
783                         // that means we're right on top of it.
784                         // We can assume that -100dbm is a sane floor value for
785                         // the weakest signal.
786                         // If a signal is 0 it means we haven't seen it at all so
787                         // just ignore that data point
788                         // We turn signals into a 'useful' graph by clamping to
789                         // -100 and -20 and then scaling it as a positive number.
790                         var moddata = new Array();
791
792                         for (var x = 0; x < rrdata.length; x++) {
793                             var d = rrdata[x];
794
795                             if (d == 0)
796                                 moddata.push(0);
797
798                             if (d < -100)
799                                 d = -100;
800
801                             if (d > -20)
802                                 d = -20;
803
804                             // Normalize to 0-80
805                             d = (d * -1) - 20;
806
807                             // Reverse (weaker is worse), get as percentage
808                             var rs = (80 - d) / 80;
809
810                             moddata.push(100*rs);
811                         }
812
813                         rrdiv.sparkline(moddata, { type: "bar",
814                             height: 12,
815                             barColor: kismet_theme.sparkline_main,
816                             nullColor: kismet_theme.sparkline_main,
817                             zeroColor: kismet_theme.sparkline_main,
818                         });
819
820                     }
821                     */
822
823                 },
824                 {
825                     field: "kismet.device.base.signal/kismet.common.signal.last_signal",
826                     liveupdate: true,
827                     title: "Latest Signal",
828                     help: "Most recent signal level seen.  Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
829                     draw: function(opts) {
830                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
831                     },
832                     filterOnZero: true,
833                 },
834                 { 
835                     field: "kismet.device.base.signal/kismet.common.signal.last_noise",
836                     liveupdate: true,
837                     title: "Latest Noise",
838                     help: "Most recent noise level seen.  Few drivers can report noise levels.",
839                     draw: function(opts) {
840                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
841                     },
842                     filterOnZero: true,
843                 },
844                 { 
845                     field: "kismet.device.base.signal/kismet.common.signal.min_signal",
846                     liveupdate: true,
847                     title: "Min. Signal",
848                     help: "Weakest signal level seen.  Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
849                     draw: function(opts) {
850                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
851                     },
852                     filterOnZero: true,
853                 },
854
855                 { 
856                     field: "kismet.device.base.signal/kismet.common.signal.max_signal",
857                     liveupdate: true,
858                     title: "Max. Signal",
859                     help: "Strongest signal level seen.  Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
860                     draw: function(opts) {
861                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
862                     },
863                     filterOnZero: true,
864                 },
865                 { 
866                     field: "kismet.device.base.signal/kismet.common.signal.min_noise",
867                     liveupdate: true,
868                     title: "Min. Noise",
869                     filterOnZero: true,
870                     help: "Least amount of interference or noise seen.  Most capture drivers are not capable of measuring noise levels.",
871                     draw: function(opts) {
872                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
873                     },
874                 },
875                 { 
876                     field: "kismet.device.base.signal/kismet.common.signal.max_noise",
877                     liveupdate: true,
878                     title: "Max. Noise",
879                     filterOnZero: true,
880                     help: "Largest amount of interference or noise seen.  Most capture drivers are not capable of measuring noise levels.",
881                     draw: function(opts) {
882                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
883                     },
884                 },
885                 { // Pseudo-field of aggregated location, only show when the location is valid
886                     field: "kismet.device.base.signal/kismet.common.signal.peak_loc",
887                     liveupdate: true,
888                     title: "Peak Location",
889                     help: "When a GPS location is available, the peak location is the coordinates at which the strongest signal level was recorded for this device.",
890                     filter: function(opts) {
891                         return kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.fix") >= 2;
892                     },
893                     draw: function(opts) {
894                         var loc =
895                             kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[1]")) + ", " +
896                             kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[0]"));
897
898                         return loc;
899                     },
900                 },
901
902                 ],
903             },
904             {
905                 field: "group_packet_counts",
906                 groupTitle: "Packets",
907                 id: "group_packet_counts",
908
909                 fields: [
910                 {
911                     field: "graph_field_overall",
912                     span: true,
913                     liveupdate: true,
914                     render: function(opts) {
915                         var d = 
916                             $('<div>', {
917                                 style: 'width: 80%; height: 250px; padding-bottom: 5px;',
918                             })
919                             .append(
920                                 $('<canvas>', {
921                                     id: 'packetdonut',
922                                 })
923                             );
924
925                         return d;
926                     },
927                     draw: function(opts) {
928                         var legend = ['LLC/Management', 'Data'];
929                         var data = [
930                             opts['data']['kismet.device.base.packets.llc'],
931                             opts['data']['kismet.device.base.packets.data'],
932                         ];
933                         var colors = [
934                             'rgba(46, 99, 162, 1)',
935                             'rgba(96, 149, 212, 1)',
936                         ];
937
938                         var barChartData = {
939                             labels: legend,
940
941                             datasets: [{
942                                 label: 'Dataset 1',
943                                 backgroundColor: colors,
944                                 borderWidth: 0,
945                                 data: data,
946                             }],
947                         };
948
949                         if ('packetdonut' in window[storage]) {
950                             window[storage].packetdonut.data.datasets[0].data = data;
951                             window[storage].packetdonut.update();
952                         } else {
953                             window[storage].packetdonut = 
954                                 new Chart($('canvas', opts['container']), {
955                                     type: 'doughnut',
956                                     data: barChartData,
957                                     options: {
958                                         global: {
959                                             maintainAspectRatio: false,
960                                         },
961                                         animation: false,
962                                         legend: {
963                                             display: true,
964                                         },
965                                         title: {
966                                             display: true,
967                                             text: 'Packet Types'
968                                         },
969                                         height: '200px',
970                                     }
971                                 });
972
973                             window[storage].packetdonut.render();
974                         }
975                     },
976                 },
977                 {
978                     field: "kismet.device.base.packets.total",
979                     liveupdate: true,
980                     title: "Total Packets",
981                     help: "Count of all packets of all types",
982                 },
983                 {
984                     field: "kismet.device.base.packets.llc",
985                     liveupdate: true,
986                     title: "LLC/Management",
987                     help: "LLC (Link Layer Control) and Management packets are typically used for controlling and defining wireless networks.  Typically they do not carry data.",
988                 },
989                 {
990                     field: "kismet.device.base.packets.error",
991                     liveupdate: true,
992                     title: "Error/Invalid",
993                     help: "Error and invalid packets indicate a packet was received and was partially processable, but was damaged or incorrect in some way.  Most error packets are dropped completely as it is not possible to associate them with a specific device.",
994                 },
995                 {
996                     field: "kismet.device.base.packets.data",
997                     liveupdate: true,
998                     title: "Data",
999                     help: "Data frames carry messages and content for the device.",
1000                 },
1001                 {
1002                     field: "kismet.device.base.packets.crypt",
1003                     liveupdate: true,
1004                     title: "Encrypted",
1005                     help: "Some data frames can be identified by Kismet as carrying encryption, either by the contents or by packet flags, depending on the phy type",
1006                 },
1007                 {
1008                     field: "kismet.device.base.packets.filtered",
1009                     liveupdate: true,
1010                     title: "Filtered",
1011                     help: "Filtered packets are ignored by Kismet",
1012                 },
1013                 {
1014                     field: "kismet.device.base.datasize",
1015                     liveupdate: true,
1016                     title: "Data Transferred",
1017                     help: "Amount of data transferred",
1018                     draw: function(opts) {
1019                         return kismet.HumanReadableSize(opts['value']);
1020                     }
1021                 }
1022
1023
1024                 ]
1025             },
1026
1027             {
1028                 // Location is its own group
1029                 groupTitle: "Avg. Location",
1030                 // Spoofed field for ID purposes
1031                 field: "group_avg_location",
1032                 // Sub-table ID
1033                 id: "group_avg_location",
1034
1035                 // Don't show location if we don't know it
1036                 filter: function(opts) {
1037                     return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 2);
1038                 },
1039
1040                 // Fields in subgroup
1041                 fields: [
1042                 {
1043                     field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.geopoint",
1044                     title: "Location",
1045                     draw: function(opts) {
1046                         try {
1047                             if (opts['value'][1] == 0 || opts['value'][0] == 0)
1048                                 return "<i>Unknown</i>";
1049
1050                             return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
1051                         } catch (error) {
1052                             return "<i>Unknown</i>";
1053                         }
1054                     }
1055                 },
1056                 {
1057                     field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.alt",
1058                     title: "Altitude",
1059                     filter: function(opts) {
1060                         return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 3);
1061                     },
1062                     draw: function(opts) {
1063                         try {
1064                             return kismet_ui.renderHeightDistance(opts['value']);
1065                         } catch (error) {
1066                             return "<i>Unknown</i>";
1067                         }
1068                     },
1069                 }
1070                 ],
1071             }
1072             ]
1073         }, storage);
1074     }
1075 });
1076
1077 kismet_ui.AddDeviceDetail("packets", "Packet Graphs", 10, {
1078     render: function(data) {
1079         // Make 3 divs for s, m, h RRD
1080         var ret = 
1081             '<b>Packet Rates</b><br /><br />' +
1082             'Packets per second (last minute)<br /><div /><br />' +
1083             'Packets per minute (last hour)<br /><div /><br />' +
1084             'Packets per hour (last day)<br /><div />';
1085
1086         if ('kismet.device.base.datasize.rrd' in data)
1087             ret += '<br /><b>Data</b><br /><br />' +
1088             'Data per second (last minute)<br /><div /><br />' +
1089             'Data per minute (last hour)<br /><div /><br />' +
1090             'Data per hour (last day)<br /><div />';
1091
1092         return ret;
1093     },
1094     draw: function(data, target) {
1095         var m = $('div:eq(0)', target);
1096         var h = $('div:eq(1)', target);
1097         var d = $('div:eq(2)', target);
1098
1099         var dm = $('div:eq(3)', target);
1100         var dh = $('div:eq(4)', target);
1101         var dd = $('div:eq(5)', target);
1102
1103         var mdata = [];
1104         var hdata = [];
1105         var ddata = [];
1106
1107         if (('kismet.device.base.packets.rrd' in data)) {
1108             mdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_SECOND);
1109             hdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_MINUTE);
1110             ddata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_HOUR);
1111
1112             m.sparkline(mdata, { type: "bar",
1113                     height: 12,
1114                 barColor: kismet_theme.sparkline_main,
1115                 nullColor: kismet_theme.sparkline_main,
1116                 zeroColor: kismet_theme.sparkline_main,
1117                 });
1118             h.sparkline(hdata,
1119                 { type: "bar",
1120                     height: 12,
1121                     barColor: kismet_theme.sparkline_main,
1122                     nullColor: kismet_theme.sparkline_main,
1123                     zeroColor: kismet_theme.sparkline_main,
1124                 });
1125             d.sparkline(ddata,
1126                 { type: "bar",
1127                     height: 12,
1128                     barColor: kismet_theme.sparkline_main,
1129                     nullColor: kismet_theme.sparkline_main,
1130                     zeroColor: kismet_theme.sparkline_main,
1131                 });
1132         } else {
1133             m.html("<i>No packet data available</i>");
1134             h.html("<i>No packet data available</i>");
1135             d.html("<i>No packet data available</i>");
1136         }
1137             
1138
1139         if ('kismet.device.base.datasize.rrd' in data) {
1140             var dmdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_SECOND);
1141             var dhdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_MINUTE);
1142             var dddata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_HOUR);
1143
1144         dm.sparkline(dmdata,
1145             { type: "bar",
1146                 height: 12,
1147                 barColor: kismet_theme.sparkline_main,
1148                 nullColor: kismet_theme.sparkline_main,
1149                 zeroColor: kismet_theme.sparkline_main,
1150             });
1151         dh.sparkline(dhdata,
1152             { type: "bar",
1153                 height: 12,
1154                 barColor: kismet_theme.sparkline_main,
1155                 nullColor: kismet_theme.sparkline_main,
1156                 zeroColor: kismet_theme.sparkline_main,
1157             });
1158         dd.sparkline(dddata,
1159             { type: "bar",
1160                 height: 12,
1161                 barColor: kismet_theme.sparkline_main,
1162                 nullColor: kismet_theme.sparkline_main,
1163                 zeroColor: kismet_theme.sparkline_main,
1164             });
1165         }
1166
1167     }
1168 });
1169
1170 kismet_ui.AddDeviceDetail("seenby", "Seen By", 900, {
1171     filter: function(data) {
1172         return (Object.keys(data['kismet.device.base.seenby']).length > 0);
1173     },
1174     draw: function(data, target, options, storage) {
1175         target.devicedata(data, {
1176             id: "seenbyDeviceData",
1177
1178             fields: [
1179             {
1180                 field: "kismet.device.base.seenby",
1181                 id: "seenby_group",
1182                 groupIterate: true,
1183                 iterateTitle: function(opts) {
1184                     var this_uuid = opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1185                     $.get(`${local_uri_prefix}datasource/by-uuid/${this_uuid}/source.json`)
1186                     .done(function(dsdata) {
1187                         dsdata = kismet.sanitizeObject(dsdata);
1188                         opts['title'].html(`${dsdata['kismet.datasource.name']} (${dsdata['kismet.datasource.capture_interface']}) ${dsdata['kismet.datasource.uuid']}`);
1189                     })
1190                     return opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1191                 },
1192                 fields: [
1193                 {
1194                     field: "kismet.common.seenby.uuid",
1195                     title: "UUID",
1196                     empty: "<i>None</i>"
1197                 },
1198                 {
1199                     field: "kismet.common.seenby.first_time",
1200                     title: "First Seen",
1201                     draw: kismet_ui.RenderTrimmedTime,
1202                 },
1203                 {
1204                     field: "kismet.common.seenby.last_time",
1205                     title: "Last Seen",
1206                     draw: kismet_ui.RenderTrimmedTime,
1207                 },
1208                 ]
1209             }]
1210         });
1211     },
1212 });
1213
1214 kismet_ui.AddDeviceDetail("devel", "Dev/Debug Options", 10000, {
1215     render: function(data) {
1216         return 'Device JSON: <a href="devices/by-key/' + data['kismet.device.base.key'] + '/device.prettyjson" target="_new">link</a><br />';
1217     }});
1218
1219 /* Sidebar:  Memory monitor
1220  *
1221  * The memory monitor looks at system_status and plots the amount of
1222  * ram vs number of tracked devices from the RRD
1223  */
1224 kismet_ui_sidebar.AddSidebarItem({
1225     id: 'memory_sidebar',
1226     listTitle: '<i class="fa fa-tasks"></i> Memory Monitor',
1227     clickCallback: function() {
1228         exports.MemoryMonitor();
1229     },
1230 });
1231
1232 /*
1233 kismet_ui_sidebar.AddSidebarItem({
1234     id: 'pcap_sidebar',
1235     priority: 10000,
1236     listTitle: '<i class="fa fa-download"></i> Download Pcap-NG',
1237     clickCallback: function() {
1238         location.href = "datasource/pcap/all_sources.pcapng";
1239     },
1240 });
1241 */
1242
1243 var memoryupdate_tid;
1244 var memory_panel = null;
1245 var memory_chart = null;
1246
1247 exports.MemoryMonitor = function() {
1248     var w = $(window).width() * 0.75;
1249     var h = $(window).height() * 0.5;
1250     var offty = 20;
1251
1252     if ($(window).width() < 450 || $(window).height() < 450) {
1253         w = $(window).width() - 5;
1254         h = $(window).height() - 5;
1255         offty = 0;
1256     }
1257
1258     memory_chart = null;
1259
1260     var content = 
1261         $('<div>', {
1262             'style': 'width: 100%; height: 100%;'
1263         })
1264         .append(
1265             $('<div>', {
1266                 "style": "position: absolute; top: 0px; right: 10px; float: right;"
1267             })
1268             .append(
1269                 $('<span>', {
1270                     'id': 'k_mm_devs',
1271                     'class': 'padded',
1272                 }).html('#devices')
1273             )
1274             .append(
1275                 $('<span>', {
1276                     'id': 'k_mm_ram',
1277                     'class': 'padded',
1278                 }).html('#ram')
1279             )
1280         )
1281         .append(
1282             $('<canvas>', {
1283                 'id': 'k-mm-canvas',
1284                 'style': 'k-mm-canvas'
1285             })
1286         );
1287
1288     memory_panel = $.jsPanel({
1289         id: 'memory',
1290         headerTitle: '<i class="fa fa-tasks" /> Memory use',
1291         headerControls: {
1292             controls: 'closeonly',
1293             iconfont: 'jsglyph',
1294         },
1295         content: content,
1296         onclosed: function() {
1297             clearTimeout(memoryupdate_tid);
1298         }
1299     }).resize({
1300         width: w,
1301         height: h
1302     }).reposition({
1303         my: 'center-top',
1304         at: 'center-top',
1305         of: 'window',
1306         offsetY: offty
1307     });
1308
1309     memorydisplay_refresh();
1310 }
1311
1312 function memorydisplay_refresh() {
1313     clearTimeout(memoryupdate_tid);
1314
1315     if (memory_panel == null)
1316         return;
1317
1318     if (memory_panel.is(':hidden'))
1319         return;
1320
1321     $.get(local_uri_prefix + "system/status.json")
1322     .done(function(data) {
1323         // Common rrd type and source field
1324         var rrdtype = kismet.RRD_MINUTE;
1325         var rrddata = 'kismet.common.rrd.hour_vec';
1326
1327         // Common point titles
1328         var pointtitles = new Array();
1329
1330         for (var x = 60; x > 0; x--) {
1331             if (x % 5 == 0) {
1332                 pointtitles.push(x + 'm');
1333             } else {
1334                 pointtitles.push(' ');
1335             }
1336         }
1337
1338         var mem_linedata =
1339             kismet.RecalcRrdData2(data['kismet.system.memory.rrd'], rrdtype);
1340
1341         for (var p in mem_linedata) {
1342             mem_linedata[p] = Math.round(mem_linedata[p] / 1024);
1343         }
1344
1345         var dev_linedata =
1346             kismet.RecalcRrdData2(data['kismet.system.devices.rrd'], rrdtype);
1347
1348         $('#k_mm_devs', memory_panel.content).html(`${dev_linedata[dev_linedata.length - 1]} devices`);
1349         $('#k_mm_ram', memory_panel.content).html(`${mem_linedata[mem_linedata.length - 1]} MB`);
1350
1351         if (memory_chart == null) {
1352             var datasets = [
1353                 {
1354                     label: 'Memory (MB)',
1355                     fill: 'false',
1356                     // yAxisID: 'mem-axis',
1357                     borderColor: 'black',
1358                     backgroundColor: 'transparent',
1359                     data: mem_linedata,
1360                 },
1361                 {
1362                     label: 'Devices',
1363                     fill: 'false',
1364                     // yAxisID: 'dev-axis',
1365                     borderColor: 'blue',
1366                     backgroundColor: 'rgba(100, 100, 255, 0.33)',
1367                     data: dev_linedata,
1368                 }
1369             ];
1370
1371             var canvas = $('#k-mm-canvas', memory_panel.content);
1372
1373             memory_chart = new Chart(canvas, {
1374                 type: 'line',
1375                 options: {
1376                     responsive: true,
1377                     maintainAspectRatio: false,
1378                     scales: {
1379                         yAxes: [
1380                             {
1381                                 position: "left",
1382                                 "id": "mem-axis",
1383                                 ticks: {
1384                                     beginAtZero: true,
1385                                 }
1386                             },
1387 /*                          {
1388                                 position: "right",
1389                                 "id": "dev-axis",
1390                                 ticks: {
1391                                     beginAtZero: true,
1392                                 }
1393                             }
1394 */
1395                         ]
1396                     },
1397                 },
1398                 data: {
1399                     labels: pointtitles,
1400                     datasets: datasets
1401                 }
1402             });
1403
1404         } else {
1405             memory_chart.data.datasets[0].data = mem_linedata;
1406             memory_chart.data.datasets[1].data = dev_linedata;
1407             // memory_chart.data.datasets = datasets;
1408             memory_chart.data.labels = pointtitles;
1409             memory_chart.update();
1410         }
1411     })
1412     .always(function() {
1413         memoryupdate_tid = setTimeout(memorydisplay_refresh, 5000);
1414     });
1415 };
1416
1417
1418 /* Sidebar:  Packet queue display
1419  *
1420  * Packet queue display graphs the amount of packets in the queue, the amount dropped, 
1421  * the # of duplicates, and so on
1422  */
1423 kismet_ui_sidebar.AddSidebarItem({
1424     id: 'packetqueue_sidebar',
1425     listTitle: '<i class="fa fa-area-chart"></i> Packet Rates',
1426     clickCallback: function() {
1427         exports.PacketQueueMonitor();
1428     },
1429 });
1430
1431 var packetqueueupdate_tid;
1432 var packetqueue_panel = null;
1433
1434 exports.PacketQueueMonitor = function() {
1435     var w = $(window).width() * 0.75;
1436     var h = $(window).height() * 0.5;
1437     var offty = 20;
1438
1439     if ($(window).width() < 450 || $(window).height() < 450) {
1440         w = $(window).width() - 5;
1441         h = $(window).height() - 5;
1442         offty = 0;
1443     }
1444
1445     var content =
1446         $('<div class="k-pqm-contentdiv">')
1447         .append(
1448             $('<div id="pqm-tabs" class="tabs-min">')
1449         );
1450
1451     packetqueue_panel = $.jsPanel({
1452         id: 'packetqueue',
1453         headerTitle: '<i class="fa fa-area-chart" /> Packet Rates',
1454         headerControls: {
1455             controls: 'closeonly',
1456             iconfont: 'jsglyph',
1457         },
1458         content: content,
1459         onclosed: function() {
1460             clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1461         }
1462     }).resize({
1463         width: w,
1464         height: h
1465     }).reposition({
1466         my: 'center-top',
1467         at: 'center-top',
1468         of: 'window',
1469         offsetY: offty
1470     });
1471
1472     packetqueue_panel.packetqueue_chart = null;
1473     packetqueue_panel.datasource_chart = null;
1474
1475     var f_pqm_packetqueue = function(div) {
1476         packetqueue_panel.pq_content = div;
1477     };
1478
1479     var f_pqm_ds = function(div) {
1480         packetqueue_panel.ds_content = div;
1481     };
1482
1483     kismet_ui_tabpane.AddTab({
1484         id: 'packetqueue',
1485         tabTitle: 'Processing Queue',
1486         createCallback: f_pqm_packetqueue,
1487         priority: -1001
1488     }, 'pqm-tabs');
1489
1490     kismet_ui_tabpane.AddTab({
1491         id: 'datasources-graph',
1492         tabTitle: 'Per Datasource',
1493         createCallback: f_pqm_ds,
1494         priority: -1000
1495     }, 'pqm-tabs');
1496
1497     kismet_ui_tabpane.MakeTabPane($('#pqm-tabs', content), 'pqm-tabs');
1498
1499     packetqueuedisplay_refresh();
1500     datasourcepackets_refresh();
1501 }
1502
1503 function packetqueuedisplay_refresh() {
1504     if (packetqueue_panel == null)
1505         return;
1506
1507     clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1508
1509     if (packetqueue_panel.is(':hidden'))
1510         return;
1511
1512     $.get(local_uri_prefix + "packetchain/packet_stats.json")
1513     .done(function(data) {
1514         // Common rrd type and source field
1515         var rrdtype = kismet.RRD_MINUTE;
1516
1517         // Common point titles
1518         var pointtitles = new Array();
1519
1520         for (var x = 60; x > 0; x--) {
1521             if (x % 5 == 0) {
1522                 pointtitles.push(x + 'm');
1523             } else {
1524                 pointtitles.push(' ');
1525             }
1526         }
1527
1528         var peak_linedata =
1529             kismet.RecalcRrdData2(data['kismet.packetchain.peak_packets_rrd'], rrdtype);
1530         var rate_linedata =
1531             kismet.RecalcRrdData2(data['kismet.packetchain.packets_rrd'], rrdtype);
1532         var queue_linedata =
1533             kismet.RecalcRrdData2(data['kismet.packetchain.queued_packets_rrd'], rrdtype);
1534         var drop_linedata =
1535             kismet.RecalcRrdData2(data['kismet.packetchain.dropped_packets_rrd'], rrdtype);
1536         var dupe_linedata =
1537             kismet.RecalcRrdData2(data['kismet.packetchain.dupe_packets_rrd'], rrdtype);
1538         var processing_linedata =
1539             kismet.RecalcRrdData2(data['kismet.packetchain.processed_packets_rrd'], rrdtype);
1540
1541         var datasets = [
1542             {
1543                 label: 'Processed',
1544                 fill: 'false',
1545                 borderColor: 'orange',
1546                 backgroundColor: 'transparent',
1547                 data: processing_linedata,
1548                 pointStyle: 'cross',
1549             },
1550             {
1551                 label: 'Incoming packets (peak)',
1552                 fill: 'false',
1553                 borderColor: kismet_theme.graphBasicColor,
1554                 backgroundColor: kismet_theme.graphBasicBackgroundColor,
1555                 data: peak_linedata,
1556             },
1557             {
1558                 label: 'Incoming packets (1 min avg)',
1559                 fill: 'false',
1560                 borderColor: 'purple',
1561                 backgroundColor: 'transparent',
1562                 data: rate_linedata,
1563                 pointStyle: 'rect',
1564             },
1565             {
1566                 label: 'Queue',
1567                 fill: 'false',
1568                 borderColor: 'blue',
1569                 backgroundColor: 'transparent',
1570                 data: queue_linedata,
1571                 pointStyle: 'cross',
1572             },
1573             {
1574                 label: 'Dropped / error packets',
1575                 fill: 'false',
1576                 borderColor: 'red',
1577                 backgroundColor: 'transparent',
1578                 data: drop_linedata,
1579                 pointStyle: 'star',
1580             },
1581             {
1582                 label: 'Duplicates',
1583                 fill: 'false',
1584                 borderColor: 'green',
1585                 backgroundColor: 'transparent',
1586                 data: dupe_linedata,
1587                 pointStyle: 'triangle',
1588             },
1589         ];
1590
1591         if (packetqueue_panel.packetqueue_chart == null) {
1592             packetqueue_panel.pq_content.append(
1593                 $('<canvas>', {
1594                     "id": "pq-canvas",
1595                     "width": "100%",
1596                     "height": "100%",
1597                     "class": "k-mm-canvas",
1598                 })
1599             );
1600
1601             var canvas = $('#pq-canvas', packetqueue_panel.pq_content);
1602
1603             packetqueue_panel.packetqueue_chart = new Chart(canvas, {
1604                 type: 'line',
1605                 options: {
1606                     responsive: true,
1607                     maintainAspectRatio: false,
1608                     scales: {
1609                         yAxis: [
1610                             {
1611                                 position: "left",
1612                                 "id": "mem-axis",
1613                                 ticks: {
1614                                     beginAtZero: true,
1615                                 }
1616                             },
1617                         ]
1618                     },
1619                 },
1620                 data: {
1621                     labels: pointtitles,
1622                     datasets: datasets
1623                 }
1624             });
1625
1626         } else {
1627             packetqueue_panel.packetqueue_chart.data.datasets = datasets;
1628             packetqueue_panel.packetqueue_chart.data.labels = pointtitles;
1629             packetqueue_panel.packetqueue_chart.update(0);
1630         }
1631     })
1632     .always(function() {
1633         packetqueue_panel.packetqueueupdate_tid = setTimeout(packetqueuedisplay_refresh, 5000);
1634     });
1635 };
1636
1637 function datasourcepackets_refresh() {
1638     if (packetqueue_panel == null)
1639         return;
1640
1641     clearTimeout(packetqueue_panel.datasourceupdate_tid);
1642
1643     if (packetqueue_panel.is(':hidden'))
1644         return;
1645
1646     $.get(local_uri_prefix + "datasource/all_sources.json")
1647     .done(function(data) {
1648         var datasets = [];
1649         var num = 0;
1650
1651         // Common point titles
1652         var pointtitles = new Array();
1653
1654         var rval = $('#pq_ds_range', packetqueue_panel.ds_content).val();
1655         var range = kismet.RRD_SECOND;
1656
1657         if (rval == "hour")
1658             range = kismet.RRD_MINUTE;
1659         if (rval == "day")
1660             range = kismet.RRD_HOUR;
1661
1662         if (range == kismet.RRD_SECOND || range == kismet.RRD_MINUTE) {
1663             for (var x = 60; x > 0; x--) {
1664                 if (x % 5 == 0) {
1665                     if (range == kismet.RRD_SECOND)
1666                         pointtitles.push(x + 's');
1667                     else
1668                         pointtitles.push(x + 'm');
1669                 } else {
1670                     pointtitles.push(' ');
1671                 }
1672             }
1673         } else {
1674             for (var x = 23; x > 0; x--) {
1675                 pointtitles.push(x + 'h');
1676             }
1677         }
1678
1679         for (var source of data) {
1680             var color = parseInt(255 * (num / data.length))
1681
1682             var linedata;
1683
1684             if ($('#pq_ds_type', packetqueue_panel.ds_content).val() == "bps")
1685                 linedata =
1686                     kismet.RecalcRrdData2(source['kismet.datasource.packets_datasize_rrd'], 
1687                     range,
1688                     {
1689                         transform: function(data, opt) {
1690                             var ret = [];
1691
1692                             for (var d of data)
1693                                 ret.push(d / 1024);
1694
1695                             return ret;
1696                         }
1697                     });
1698             else
1699                 linedata =
1700                     kismet.RecalcRrdData2(source['kismet.datasource.packets_rrd'], range);
1701
1702             datasets.push({
1703                 "label": source['kismet.datasource.name'],
1704                 "borderColor": `hsl(${color}, 100%, 50%)`,
1705                 "data": linedata,
1706                 "fill": false,
1707             });
1708
1709             num = num + 1;
1710
1711         }
1712
1713         if (packetqueue_panel.datasource_chart == null) {
1714             packetqueue_panel.ds_content.append(
1715                 $('<div>', {
1716                     "style": "position: absolute; top: 0px; right: 10px; float: right;"
1717                 })
1718                 .append(
1719                     $('<select>', {
1720                         "id": "pq_ds_type",
1721                     })
1722                     .append(
1723                         $('<option>', {
1724                             "value": "pps",
1725                             "selected": "selected",
1726                         }).text("Packets")
1727                     )
1728                     .append(
1729                         $('<option>', {
1730                             "value": "bps",
1731                         }).text("Data (kB)")
1732                     )
1733                 )
1734                 .append(
1735                     $('<select>', {
1736                         "id": "pq_ds_range",
1737                     })
1738                     .append(
1739                         $('<option>', {
1740                             "value": "second",
1741                             "selected": "selected",
1742                         }).text("Past Minute")
1743                     )
1744                     .append(
1745                         $('<option>', {
1746                             "value": "hour",
1747                         }).text("Past Hour")
1748                     )
1749                     .append(
1750                         $('<option>', {
1751                             "value": "day",
1752                         }).text("Past Day")
1753                     )
1754                 )
1755             ).append(
1756                 $('<canvas>', {
1757                     "id": "dsg-canvas",
1758                     "width": "100%",
1759                     "height": "100%",
1760                     "class": "k-mm-canvas",
1761                 })
1762             );
1763
1764             packetqueue_panel.datasource_chart = 
1765                 new Chart($('#dsg-canvas', packetqueue_panel.ds_content), {
1766                 "type": "line",
1767                 "options": {
1768                     "responsive": true,
1769                     "maintainAspectRatio": false,
1770                     "scales": {
1771                         "yAxes": [
1772                             {
1773                                 "position": "left",
1774                                 "id": "pkts-axis",
1775                                 "ticks": {
1776                                     "beginAtZero": true,
1777                                 }
1778                             },
1779                         ],
1780                     },
1781                 },
1782                 "data": {
1783                     "labels": pointtitles,
1784                     "datasets": datasets,
1785                 }
1786             });
1787         } else {
1788             packetqueue_panel.datasource_chart.data.datasets = datasets;
1789             packetqueue_panel.datasource_chart.data.labels = pointtitles;
1790             packetqueue_panel.datasource_chart.update(0);
1791         }
1792     })
1793     .always(function() {
1794         packetqueue_panel.datasourceupdate_tid = setTimeout(datasourcepackets_refresh, 1000);
1795     });
1796 };
1797
1798 // Settings options
1799
1800 kismet_ui_settings.AddSettingsPane({
1801     id: 'gps_topbar',
1802     listTitle: "GPS Status",
1803     create: function(elem) {
1804         elem.append(
1805             $('<form>', {
1806                 id: 'form'
1807             })
1808             .append(
1809                 $('<fieldset>', {
1810                     id: 'set_gps'
1811                 })
1812                 .append(
1813                     $('<legend>', {})
1814                     .html("GPS Display")
1815                 )
1816                 .append(
1817                     $('<input>', {
1818                         type: 'radio',
1819                         id: 'gps_icon',
1820                         name: 'gps_status',
1821                         value: 'icon',
1822                     })
1823                 )
1824                 .append(
1825                     $('<label>', {
1826                         for: 'gps_icon'
1827                     })
1828                     .html("Icon only")
1829                 )
1830                 .append($('<div>', { class: 'spacer' }).html(" "))
1831                 .append(
1832                     $('<input>', {
1833                         type: 'radio',
1834                         id: 'gps_text',
1835                         name: 'gps_status',
1836                         value: 'text',
1837                     })
1838                 )
1839                 .append(
1840                     $('<label>', {
1841                         for: 'gps_text'
1842                     })
1843                     .html("Text only")
1844                 )
1845                 .append($('<div>', { class: 'spacer' }).html(" "))
1846                 .append(
1847                     $('<input>', {
1848                         type: 'radio',
1849                         id: 'gps_both',
1850                         name: 'gps_status',
1851                         value: 'both',
1852                     })
1853                 )
1854                 .append(
1855                     $('<label>', {
1856                         for: 'gps_both'
1857                     })
1858                     .html("Icon and Text")
1859                 )
1860             )
1861         );
1862
1863         $('#form', elem).on('change', function() {
1864             kismet_ui_settings.SettingsModified();
1865         });
1866
1867         if (kismet.getStorage('kismet.ui.gps.icon', 'True') === 'True') {
1868             if (kismet.getStorage('kismet.ui.gps.text', 'True') === 'True') {
1869                 $('#gps_both', elem).attr('checked', 'checked');
1870             } else {
1871                 $('#gps_icon', elem).attr('checked', 'checked');
1872             }
1873         } else {
1874             $('#gps_text', elem).attr('checked', 'checked');
1875         }
1876
1877         $('#set_gps', elem).controlgroup();
1878     },
1879     save: function(elem) {
1880         var val = $("input[name='gps_status']:checked", elem).val();
1881
1882         if (val === "both") {
1883             kismet.putStorage('kismet.ui.gps.text', 'True');
1884             kismet.putStorage('kismet.ui.gps.icon', 'True');
1885         } else if (val === "text") {
1886             kismet.putStorage('kismet.ui.gps.text', 'True');
1887             kismet.putStorage('kismet.ui.gps.icon', 'False');
1888         } else if (val === "icon") {
1889             kismet.putStorage('kismet.ui.gps.icon', 'True');
1890             kismet.putStorage('kismet.ui.gps.text', 'False');
1891         }
1892     }
1893 })
1894
1895 kismet_ui_settings.AddSettingsPane({
1896     id: 'base_units_measurements',
1897     listTitle: 'Units &amp; Measurements',
1898     create: function(elem) {
1899         elem.append(
1900             $('<form>', {
1901                 id: 'form'
1902             })
1903             .append(
1904                 $('<fieldset>', {
1905                     id: 'set_distance',
1906                 })
1907                 .append(
1908                     $('<legend>', { })
1909                     .html("Distance")
1910                 )
1911                 .append(
1912                     $('<input>', {
1913                         type: 'radio',
1914                         id: 'dst_metric',
1915                         name: 'distance',
1916                         value: 'metric',
1917                     })
1918                 )
1919                 .append(
1920                     $('<label>', {
1921                         for: 'dst_metric',
1922                     })
1923                     .html('Metric')
1924                 )
1925                 .append(
1926                     $('<input>', {
1927                         type: 'radio',
1928                         id: 'dst_imperial',
1929                         name: 'distance',
1930                         value: 'imperial',
1931                     })
1932                 )
1933                 .append(
1934                     $('<label>', {
1935                         for: 'dst_imperial',
1936                     })
1937                     .html('Imperial')
1938                 )
1939             )
1940             .append(
1941                 $('<br>', { })
1942             )
1943             .append(
1944                 $('<fieldset>', {
1945                     id: 'set_speed'
1946                 })
1947                 .append(
1948                     $('<legend>', { })
1949                     .html("Speed")
1950                 )
1951                 .append(
1952                     $('<input>', {
1953                         type: 'radio',
1954                         id: 'spd_metric',
1955                         name: 'speed',
1956                         value: 'metric',
1957                     })
1958                 )
1959                 .append(
1960                     $('<label>', {
1961                         for: 'spd_metric',
1962                     })
1963                     .html('Metric')
1964                 )
1965                 .append(
1966                     $('<input>', {
1967                         type: 'radio',
1968                         id: 'spd_imperial',
1969                         name: 'speed',
1970                         value: 'imperial',
1971                     })
1972                 )
1973                 .append(
1974                     $('<label>', {
1975                         for: 'spd_imperial',
1976                     })
1977                     .html('Imperial')
1978                 )
1979             )
1980             .append(
1981                 $('<br>', { })
1982             )
1983             .append(
1984                 $('<fieldset>', {
1985                     id: 'set_temp'
1986                 })
1987                 .append(
1988                     $('<legend>', { })
1989                     .html("Temperature")
1990                 )
1991                 .append(
1992                     $('<input>', {
1993                         type: 'radio',
1994                         id: 'temp_celsius',
1995                         name: 'temp',
1996                         value: 'celsius',
1997                     })
1998                 )
1999                 .append(
2000                     $('<label>', {
2001                         for: 'temp_celsius',
2002                     })
2003                     .html('Celsius')
2004                 )
2005                 .append(
2006                     $('<input>', {
2007                         type: 'radio',
2008                         id: 'temp_fahrenheit',
2009                         name: 'temp',
2010                         value: 'fahrenheit',
2011                     })
2012                 )
2013                 .append(
2014                     $('<label>', {
2015                         for: 'temp_fahrenheit',
2016                     })
2017                     .html('Fahrenheit')
2018                 )
2019             )
2020         );
2021
2022         $('#form', elem).on('change', function() {
2023             kismet_ui_settings.SettingsModified();
2024         });
2025
2026         if (kismet.getStorage('kismet.base.unit.distance', 'metric') === 'metric') {
2027             $('#dst_metric', elem).attr('checked', 'checked');
2028         } else {
2029             $('#dst_imperial', elem).attr('checked', 'checked');
2030         }
2031
2032         if (kismet.getStorage('kismet.base.unit.speed', 'metric') === 'metric') {
2033             $('#spd_metric', elem).attr('checked', 'checked');
2034         } else {
2035             $('#spd_imperial', elem).attr('checked', 'checked');
2036         }
2037
2038         if (kismet.getStorage('kismet.base.unit.temp', 'celsius') === 'celsius') {
2039             $('#temp_celsius', elem).attr('checked', 'checked');
2040         } else {
2041             $('#temp_fahrenheit', elem).attr('checked', 'checked');
2042         }
2043
2044         $('#set_distance', elem).controlgroup();
2045         $('#set_speed', elem).controlgroup();
2046         $('#set_temp', elem).controlgroup();
2047
2048     },
2049     save: function(elem) {
2050         var dist = $("input[name='distance']:checked", elem).val();
2051         kismet.putStorage('kismet.base.unit.distance', dist);
2052         var spd = $("input[name='speed']:checked", elem).val();
2053         kismet.putStorage('kismet.base.unit.speed', spd);
2054         var tmp = $("input[name='temp']:checked", elem).val();
2055         kismet.putStorage('kismet.base.unit.temp', tmp);
2056
2057         return true;
2058     },
2059 });
2060
2061 kismet_ui_settings.AddSettingsPane({
2062     id: 'base_plugins',
2063     listTitle: 'Plugins',
2064     create: function(elem) {
2065         elem.append($('<i>').html('Loading plugin data...'));
2066
2067         $.get(local_uri_prefix + "plugins/all_plugins.json")
2068         .done(function(data) {
2069             elem.empty();
2070     
2071             if (data.length == 0) {
2072                 elem.append($('<i>').html('No plugins loaded...'));
2073             }
2074
2075             for (var pi in data) {
2076                 var pl = data[pi];
2077
2078                 var sharedlib = $('<p>');
2079
2080                 if (pl['kismet.plugin.shared_object'].length > 0) {
2081                     sharedlib.html("Native code from " + pl['kismet.plugin.shared_object']);
2082                 } else {
2083                     sharedlib.html("No native code");
2084                 }
2085
2086                 elem.append(
2087                     $('<div>', { 
2088                         class: 'k-b-s-plugin-title',
2089                     })
2090                     .append(
2091                         $('<b>', {
2092                             class: 'k-b-s-plugin-title',
2093                         })
2094                         .html(pl['kismet.plugin.name'])
2095                     )
2096                     .append(
2097                         $('<span>', { })
2098                         .html(pl['kismet.plugin.version'])
2099                     )
2100                 )
2101                 .append(
2102                     $('<div>', {
2103                         class: 'k-b-s-plugin-content',
2104                     })
2105                     .append(
2106                         $('<p>', { })
2107                         .html(pl['kismet.plugin.description'])
2108                     )
2109                     .append(
2110                         $('<p>', { })
2111                         .html(pl['kismet.plugin.author'])
2112                     )
2113                     .append(sharedlib)
2114                 );
2115             }
2116         });
2117     },
2118     save: function(elem) {
2119
2120     },
2121 });
2122
2123
2124 kismet_ui_settings.AddSettingsPane({
2125     id: 'base_login_password',
2126     listTitle: 'Login &amp; Password',
2127     create: function(elem) {
2128         elem.append(
2129             $('<form>', {
2130                 id: 'form'
2131             })
2132             .append(
2133                 $('<fieldset>', {
2134                     id: 'fs_login'
2135                 })
2136                 .append(
2137                     $('<legend>', {})
2138                     .html('Server Login')
2139                 )
2140                 .append(
2141                     $('<p>')
2142                     .html('Kismet requires a username and password for functionality which changes the server, such as adding interfaces or changing configuration, or accessing some types of data.')
2143                 )
2144                 .append(
2145                     $('<p>')
2146                     .html('The Kismet password is stored in <code>~/.kismet/kismet_httpd.conf</code> in the home directory of the user running Kismet.  You will need this password to configure data sources, download pcap and other logs, or change server-side settings.<br>This server is running as <code>' + exports.system_user + '</code>, so the password can be found in <code>~' + exports.system_user + '/.kismet/kismet_httpd.conf</code>')
2147                 )
2148                 .append(
2149                     $('<p>')
2150                     .html('If you are a guest on this server you may continue without entering an admin password, but you will not be able to perform some actions or view some data.')
2151                 )
2152                 .append(
2153                     $('<br>')
2154                 )
2155                 .append(
2156                     $('<span style="display: inline-block; width: 8em;">')
2157                     .html('User name: ')
2158                 )
2159                 .append(
2160                     $('<input>', {
2161                         type: 'text',
2162                         name: 'user',
2163                         id: 'user'
2164                     })
2165                 )
2166                 .append(
2167                     $('<br>')
2168                 )
2169                 .append(
2170                     $('<span style="display: inline-block; width: 8em;">')
2171                     .html('Password: ')
2172                 )
2173                 .append(
2174                     $('<input>', {
2175                         type: 'password',
2176                         name: 'password',
2177                         id: 'password'
2178                     })
2179                 )
2180                 .append(
2181                     $('<span>', {
2182                         id: 'pwsuccessdiv',
2183                         style: 'padding-left: 5px',
2184                     })
2185                     .append(
2186                         $('<i>', {
2187                             id: 'pwsuccess',
2188                             class: 'fa fa-refresh fa-spin',
2189                         })
2190                     )
2191                     .append(
2192                         $('<span>', {
2193                             id: 'pwsuccesstext'
2194                         })
2195                     )
2196                     .hide()
2197                 )
2198             )
2199         );
2200
2201         $('#form', elem).on('change', function() {
2202             kismet_ui_settings.SettingsModified();
2203         });
2204
2205         var checker_cb = function() {
2206             // Cancel any pending timer
2207             if (pw_check_tid > -1)
2208                 clearTimeout(pw_check_tid);
2209
2210             var checkerdiv = $('#pwsuccessdiv', elem);
2211             var checker = $('#pwsuccess', checkerdiv);
2212             var checkertext = $('#pwsuccesstext', checkerdiv);
2213
2214             checker.removeClass('fa-exclamation-circle');
2215             checker.removeClass('fa-check-square');
2216
2217             checker.addClass('fa-spin');
2218             checker.addClass('fa-refresh');
2219             checkertext.text("  Checking...");
2220
2221             checkerdiv.show();
2222
2223             // Set a timer for a second from now to call the actual check 
2224             // in case the user is still typing
2225             pw_check_tid = setTimeout(function() {
2226                 exports.LoginCheck(function(success) {
2227                     if (!success) {
2228                         checker.removeClass('fa-check-square');
2229                         checker.removeClass('fa-spin');
2230                         checker.removeClass('fa-refresh');
2231                         checker.addClass('fa-exclamation-circle');
2232                         checkertext.text("  Invalid login");
2233                     } else {
2234                         checker.removeClass('fa-exclamation-circle');
2235                         checker.removeClass('fa-spin');
2236                         checker.removeClass('fa-refresh');
2237                         checker.addClass('fa-check-square');
2238                         checkertext.text("");
2239                     }
2240                 }, $('#user', elem).val(), $('#password', elem).val());
2241             }, 1000);
2242         };
2243
2244         var pw_check_tid = -1;
2245         jQuery('#password', elem).on('input propertychange paste', function() {
2246             kismet_ui_settings.SettingsModified();
2247             checker_cb();
2248         });
2249         jQuery('#user', elem).on('input propertychange paste', function() {
2250             kismet_ui_settings.SettingsModified();
2251             checker_cb();
2252         });
2253
2254         $('#user', elem).val(kismet.getStorage('kismet.base.login.username', 'kismet'));
2255         $('#password', elem).val(kismet.getStorage('kismet.base.login.password', 'kismet'));
2256
2257         if ($('#user', elem).val() === 'kismet' &&
2258         $('#password', elem).val() === 'kismet') {
2259             $('#defaultwarning').show();
2260         }
2261
2262         $('fs_login', elem).controlgroup();
2263
2264         // Check the current pw
2265         checker_cb();
2266     },
2267     save: function(elem) {
2268         kismet.putStorage('kismet.base.login.username', $('#user', elem).val());
2269         kismet.putStorage('kismet.base.login.password', $('#password', elem).val());
2270     },
2271 });
2272
2273 function show_role_help(role) {
2274     var rolehelp = `Unknown role ${role}; this could be assigned as a custom role for a Kismet plugin.`;
2275
2276     if (role === "admin")
2277         rolehelp = "The admin role is assigned to the primary web interface, external API plugins which automatically request API access, and other privileged instances.  The admin role has access to all endpoints.";
2278     else if (role === "readonly")
2279         rolehelp = "The readonly role has access to any endpoint which does not modify data.  It can not issue commands to the Kismet server, configure sources, or alter devices.  The readonly role is well suited for external data gathering from a Kismet server.";
2280     else if (role === "datasource")
2281         rolehelp = "The datasource role allows remote capture over websockets.  This role only has access to the remote capture datasource endpoint.";
2282     else if (role === "scanreport")
2283         rolehelp = "The scanreport role allows device scan reports.  This role only has access to the scan report endpoint."
2284     else if (role === "ADSB")
2285         rolehelp = "The ADSB role allows access to the combined and device-specific ADSB feeds."
2286     else if (role === "__explain__") {
2287         rolehelp = "<p>Kismet uses a basic role system to restrict access to API endpoints.  The default roles are:";
2288         rolehelp += "<p>&quot;admin&quot; which has access to all API endpoints.";
2289         rolehelp += "<p>&quot;readonly&quot; which only has access to endpoints which do not alter devices or change the configuration of the server";
2290         rolehelp += "<p>&quot;datasource&quot; which is used for websockets based remote capture and may not access any other endpoints";
2291         rolehelp += "<p>&quot;scanreport&quot; which is used for reporting scanning-mode devices";
2292         rolehelp += "<p>&quot;ADSB&quot; which is used for sharing ADSB feeds";
2293         rolehelp += "<p>Plugins or other code may define other roles.";
2294
2295         role = "Kismet API Roles";
2296     }
2297
2298     var h = $(window).height() / 4;
2299     var w = $(window).width() / 2;
2300
2301     if (w < 450) 
2302         w = $(window).width() - 5;
2303
2304     if (h < 200)
2305         h = $(window).height() - 5;
2306
2307     $.jsPanel({
2308         id: "item-help",
2309         headerTitle: `Role: ${role}`,
2310         headerControls: {
2311             controls: 'closeonly',
2312             iconfont: 'jsglyph',
2313         },
2314         contentSize: `${w} auto`,
2315         paneltype: 'modal',
2316         content: `<div style="padding: 10px;"><h3>${role}</h3><p>${rolehelp}`,
2317     })
2318     .reposition({
2319         my: 'center',
2320         at: 'center',
2321         of: 'window'
2322     });
2323 }
2324
2325 function delete_role(rolename, elem) {
2326     var deltd = $('.deltd', elem);
2327
2328     var delbt = 
2329         $('<button>', {
2330             'style': 'background-color: #DDAAAA',
2331         })
2332         .html(`Delete role &quot;${rolename}&quot;`)
2333         .button()
2334         .on('click', function() {
2335             var pd = {
2336                 'name': rolename,
2337             };
2338
2339             var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2340
2341             $.post(local_uri_prefix + "auth/apikey/revoke.cmd", postdata)
2342             .done(function(data) {
2343                 var delt = elem.parent();
2344
2345                 elem.remove();
2346
2347                 if ($('tr', delt).length == 1) {
2348                     delt.append(
2349                         $('<tr>', {
2350                             'class': 'noapi'
2351                         })
2352                         .append(
2353                             $('<td>', {
2354                                 'colspan': 4
2355                             })
2356                             .html("<i>No API keys defined...</i>")
2357                         )
2358                     );
2359                 }
2360             })
2361         });
2362
2363     deltd.empty();
2364     deltd.append(delbt);
2365
2366 }
2367
2368 function make_role_help_closure(role) {
2369     return function() { show_role_help(role); };
2370 }
2371
2372 function make_role_delete_closure(rolename, elem) {
2373     return function() { delete_role(rolename, elem); };
2374 }
2375
2376 kismet_ui_settings.AddSettingsPane({
2377     id: 'base_api_logins',
2378     listTitle: "API Keys",
2379     create: function(elem) {
2380         elem.append($("p").html("Fetching API data..."));
2381
2382         $.get(local_uri_prefix + "auth/apikey/list.json")
2383         .done(function(data) {
2384             data = kismet.sanitizeObject(data);
2385             elem.empty();
2386
2387             var tb = $('<table>', {
2388                 'class': 'apitable',
2389                 'id': 'apikeytable',
2390             })
2391
2392             .append(
2393                 $('<tr>')
2394                 .append(
2395                     $('<th>', {
2396                         'class': 'apith',
2397                         'style': 'width: 16em;',
2398                     }).html("Name")
2399                 )
2400                 .append(
2401                     $('<th>', {
2402                         'class': 'apith',
2403                         'style': 'width: 8em;',
2404                     }).html("Role")
2405                 )
2406                 .append(
2407                     $('<th>', {
2408                         'class': 'apith',
2409                         'style': 'width: 30em;',
2410                     }).html("Key")
2411                 )
2412                 .append(
2413                     $('<th>')
2414                 )
2415             );
2416
2417             elem.append(tb);
2418
2419             if (data.length == 0) {
2420                 tb.append(
2421                     $('<tr>', {
2422                         'class': 'noapi'
2423                     })
2424                     .append(
2425                         $('<td>', {
2426                             'colspan': 4
2427                         })
2428                         .html("<i>No API keys defined...</i>")
2429                     )
2430                 );
2431             }
2432
2433             for (var user of data) {
2434                 var name = user['kismet.httpd.auth.name'];
2435                 var role = user['kismet.httpd.auth.role'];
2436
2437                 var key;
2438
2439                 if ('kismet.httpd.auth.token' in user) {
2440                     key = user['kismet.httpd.auth.token'];
2441                 } else {
2442                     key = "<i>Viewing auth tokens is disabled in the Kismet configuration.</i>";
2443                 }
2444
2445                 var tr = 
2446                     $('<tr>', {
2447                         'class': 'apihover'
2448                     });
2449
2450                 tr
2451                     .append(
2452                         $('<td>').html(name)
2453                     )
2454                     .append(
2455                         $('<td>').html(role)
2456                         .append(
2457                             $('<i>', {
2458                                 'class': 'pseudolink fa fa-question-circle',
2459                                 'style': 'padding-left: 5px;',
2460                             })
2461                             .on('click', make_role_help_closure(role))
2462                         )
2463                     )
2464                     .append(
2465                         $('<td>')
2466                         .append(
2467                             $('<input>', {
2468                                 'type': 'text',
2469                                 'value': key,
2470                                 'readonly': 'true',
2471                                 'size': 34,
2472                                 'id': name.replace(" ", "_"),
2473                             })
2474                         )
2475                         .append(
2476                             $('<i>', {
2477                                 'class': 'copyuri pseudolink fa fa-copy',
2478                                 'style': 'padding-left: 5px;',
2479                                 'data-clipboard-target': `#${name.replace(" ", "_")}`, 
2480                             })
2481                         )
2482                     )
2483                     .append(
2484                         $('<td>', {
2485                             'class': 'deltd'
2486                         })
2487                         .append(
2488                             $('<i>', {
2489                                 'class': 'pseudolink fa fa-trash',
2490                             })
2491                             .on('click', make_role_delete_closure(name, tr))
2492                         )
2493                     )
2494
2495                 tb.append(
2496                     tr
2497                 )
2498             }
2499
2500             var adddiv = 
2501                 $('<div>', {
2502                     'id': 'addapidiv'
2503                 })
2504                 .append(
2505                     $('<fieldset>')
2506                     .append(
2507                         $('<button>', {
2508                             'id': 'addapikeybutton',
2509                             'class': 'padded',
2510                         }).html(`<i class="fa fa-plus"> Create API Key`)
2511                     )
2512                     .append(
2513                         $('<label>', {
2514                             'for': 'addapiname',
2515                             'class': 'padded',
2516                         }).html("Name")
2517                     )
2518                     .append(
2519                         $('<input>', {
2520                         'name': 'addapiname',
2521                         'id': 'addapiname',
2522                         'type': 'text',
2523                         'size': 16,
2524                         })
2525                     )
2526                     .append(
2527                         $('<label>', {
2528                             'for': 'addapirole',
2529                             'class': 'padded',
2530                         }).html("Role")
2531                     )
2532                     .append(
2533                         $('<select>', {
2534                             'name': 'addapirole',
2535                             'id': 'addapirole'
2536                         })
2537                         .append(
2538                             $('<option>', {
2539                                 'value': 'readonly',
2540                                 'selected': 'true',
2541                             }).html("readonly")
2542                         )
2543                         .append(
2544                             $('<option>', {
2545                                 'value': 'datasource',
2546                             }).html("datasource")
2547                         )
2548                         .append(
2549                             $('<option>', {
2550                                 'value': 'scanreport',
2551                             }).html("scanreport")
2552                         )
2553                         .append(
2554                             $('<option>', {
2555                                 'value': 'admin',
2556                             }).html("admin")
2557                         )
2558                         .append(
2559                             $('<option>', {
2560                                 'value': 'ADSB',
2561                             }).html("ADSB")
2562                         )
2563                         .append(
2564                             $('<option>', {
2565                                 'value': 'custom',
2566                             }).html("<i>custom</i>")
2567                         )
2568                     )
2569                     .append(
2570                         $('<input>', {
2571                             'name': 'addapiroleother',
2572                             'id': 'addapiroleother',
2573                             'type': 'text',
2574                             'size': 16,
2575                         }).hide()
2576                     )
2577                     .append(
2578                         $('<i>', {
2579                             'class': 'pseudolink fa fa-question-circle',
2580                             'style': 'padding-left: 5px;',
2581                         })
2582                         .on('click', make_role_help_closure("__explain__"))
2583                     )
2584                     .append(
2585                         $('<div>', {
2586                             'id': 'addapierror',
2587                             'style': 'color: red;'
2588                         }).hide()
2589                     )
2590                 );
2591
2592             $('#addapikeybutton', adddiv)
2593                 .button()
2594                 .on('click', function() {
2595                     var name = $('#addapiname').val();
2596                     var role_select = $('#addapirole option:selected').text();
2597                     var role_input = $('#addapiroleother').val();
2598
2599                     if (name.length == 0) {
2600                         $('#addapierror').show().html("Missing name.");
2601                         return;
2602                     }
2603
2604                     if (role_select === "custom" && role_input.length == 0) {
2605                         $('#addapierror').show().html("Missing custom role.");
2606                         return;
2607                     }
2608
2609                     $('#addapierror').hide();
2610
2611                     var role = role_select;
2612
2613                     if (role_select === "custom")
2614                         role = role_input;
2615
2616                     var pd = {
2617                         'name': name,
2618                         'role': role,
2619                         'duration': 0,
2620                     };
2621
2622                     var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2623
2624                     $.post(local_uri_prefix + "auth/apikey/generate.cmd", postdata)
2625                     .fail(function(response) {
2626                         var rt = kismet.sanitizeObject(response.responseText);
2627                         $('#addapierror').show().html(`Failed to add API key: ${rt}`);
2628                     })
2629                     .done(function(data) {
2630                         var key = kismet.sanitizeObject(data);
2631
2632                         var tr = 
2633                             $('<tr>', {
2634                                 'class': 'apihover'
2635                             });
2636
2637                         tr
2638                             .append(
2639                                 $('<td>').html(name)
2640                             )
2641                             .append(
2642                                 $('<td>').html(role)
2643                                 .append(
2644                                     $('<i>', {
2645                                         'class': 'pseudolink fa fa-question-circle',
2646                                         'style': 'padding-left: 5px;',
2647                                     })
2648                                     .on('click', make_role_help_closure(role))
2649                                 )
2650                             )
2651                             .append(
2652                                 $('<td>')
2653                                 .append(
2654                                     $('<input>', {
2655                                         'type': 'text',
2656                                         'value': key,
2657                                         'readonly': 'true',
2658                                         'size': 34,
2659                                         'id': name.replace(" ", "_"),
2660                                     })
2661                                 )
2662                                 .append(
2663                                     $('<i>', {
2664                                         'class': 'copyuri pseudolink fa fa-copy',
2665                                         'style': 'padding-left: 5px;',
2666                                         'data-clipboard-target': `#${name.replace(" ", "_")}`, 
2667                                     })
2668                                 )
2669                             )
2670                             .append(
2671                                 $('<td>', {
2672                                     'class': 'deltd'
2673                                 })
2674                                 .append(
2675                                     $('<i>', {
2676                                         'class': 'pseudolink fa fa-trash',
2677                                     })
2678                                     .on('click', make_role_delete_closure(name, tr))
2679                                 )
2680                             );
2681
2682                         $('#apikeytable').append(tr);
2683
2684                         $('#addapiname').val('');
2685                         $("#addapirole").prop("selectedIndex", 0);
2686                         $("#addapirole").show();
2687                         $("#addapiroleother").val('').hide();
2688                     });
2689                 });
2690
2691             $('#addapirole', adddiv).on('change', function(e) {
2692                 var val = $("#addapirole option:selected" ).text();
2693
2694                 if (val === "custom") {
2695                     $(this).hide();
2696                     $('#addapiroleother').show();
2697                 }
2698
2699             });
2700
2701             elem.append(adddiv);
2702
2703             new ClipboardJS('.copyuri');
2704         });
2705     },
2706     save: function(elem) {
2707         return true;
2708     },
2709 });
2710
2711
2712
2713 /* Add the messages and channels tabs */
2714 kismet_ui_tabpane.AddTab({
2715     id: 'messagebus',
2716     tabTitle: 'Messages',
2717     createCallback: function(div) {
2718         div.messagebus();
2719     },
2720     priority: -1001,
2721 }, 'south');
2722
2723 kismet_ui_tabpane.AddTab({
2724     id: 'channels',
2725     tabTitle: 'Channels',
2726     expandable: true,
2727     createCallback: function(div) {
2728         div.channels();
2729     },
2730     priority: -1000,
2731 }, 'south');
2732
2733 kismet_ui_tabpane.AddTab({
2734     id: 'devices',
2735     tabTitle: 'Devices',
2736     expandable: false,
2737     createCallback: function(div) {
2738         div.append(
2739                 $('<table>', {
2740                     id: 'devices',
2741                     class: 'fixeddt stripe hover nowrap pageResize',
2742                     'cell-spacing': 0,
2743                     width: '100%',
2744                 })
2745         );
2746
2747         kismet_ui.CreateDeviceTable($('#devices', div));
2748     },
2749     priority: -1000000,
2750 }, 'center');
2751
2752
2753 exports.DeviceSignalDetails = function(key) {
2754     var w = $(window).width() * 0.75;
2755     var h = $(window).height() * 0.5;
2756
2757     var devsignal_chart = null;
2758
2759     var devsignal_tid = -1;
2760
2761     var content =
2762         $('<div>', {
2763             class: 'k-dsd-container'
2764         })
2765         .append(
2766             $('<div>', {
2767                 class: 'k-dsd-info'
2768             })
2769             .append(
2770                 $('<div>', {
2771                     class: 'k-dsd-title'
2772                 })
2773                 .html("Signal")
2774             )
2775             .append(
2776                 $('<table>', {
2777                     class: 'k-dsd-table'
2778                 })
2779                 .append(
2780                     $('<tr>', {
2781                     })
2782                     .append(
2783                         $('<td>', {
2784                             width: '50%'
2785                         })
2786                         .html("Last Signal:")
2787                     )
2788                     .append(
2789                         $('<td>', {
2790                             width: '50%',
2791                         })
2792                         .append(
2793                             $('<span>', {
2794                                 class: 'k-dsd-lastsignal',
2795                             })
2796                         )
2797                         .append(
2798                             $('<i>', {
2799                                 class: 'fa k-dsd-arrow k-dsd-arrow-down',
2800                             })
2801                             .hide()
2802                         )
2803                     )
2804                 )
2805                 .append(
2806                     $('<tr>', {
2807                     })
2808                     .append(
2809                         $('<td>', {
2810                             width: '50%'
2811                         })
2812                         .html("Min Signal:")
2813                     )
2814                     .append(
2815                         $('<td>', {
2816                             width: '50%',
2817                             class: 'k-dsd-minsignal',
2818                         })
2819                         .html("n/a")
2820                     )
2821                 )
2822                 .append(
2823                     $('<tr>', {
2824                     })
2825                     .append(
2826                         $('<td>', {
2827                             width: '50%'
2828                         })
2829                         .html("Max Signal:")
2830                     )
2831                     .append(
2832                         $('<td>', {
2833                             width: '50%',
2834                             class: 'k-dsd-maxsignal',
2835                         })
2836                         .html("n/a")
2837                     )
2838                 )
2839             )
2840         )
2841         .append(
2842             $('<div>', {
2843                 class: 'k-dsd-graph'
2844             })
2845             .append(
2846                 $('<canvas>', {
2847                     id: 'k-dsd-canvas',
2848                     class: 'k-dsd-canvas'
2849                 })
2850             )
2851         );
2852
2853     var devsignal_panel = $.jsPanel({
2854         id: 'devsignal' + key,
2855         headerTitle: '<i class="fa fa-signal" /> Signal',
2856         headerControls: {
2857             iconfont: 'jsglyph',
2858         },
2859         content: content,
2860         onclosed: function() {
2861             clearTimeout(devsignal_tid);
2862         }
2863     }).resize({
2864         width: w,
2865         height: h
2866     }).reposition({
2867         my: 'center-top',
2868         at: 'center-top',
2869         of: 'window',
2870         offsetY: 20
2871     });
2872
2873     var emptyminute = new Array();
2874     for (var x = 0; x < 60; x++) {
2875         emptyminute.push(0);
2876     }
2877
2878     devsignal_tid = devsignal_refresh(key, devsignal_panel,
2879         devsignal_chart, devsignal_tid, 0, emptyminute);
2880 }
2881
2882 function devsignal_refresh(key, devsignal_panel, devsignal_chart,
2883     devsignal_tid, lastsignal, fakerrd) {
2884     clearTimeout(devsignal_tid);
2885
2886     if (devsignal_panel == null)
2887         return;
2888
2889     if (devsignal_panel.is(':hidden'))
2890         return;
2891
2892     var signal = lastsignal;
2893
2894     $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
2895     .done(function(data) {
2896         var title = '<i class="fa fa-signal" /> Signal ' +
2897             kismet.censorMAC(data['kismet.device.base.macaddr']) + ' ' +
2898             kismet.censorMAC(data['kismet.device.base.name']);
2899         devsignal_panel.headerTitle(title);
2900
2901         var sigicon = $('.k-dsd-arrow', devsignal_panel.content);
2902
2903         sigicon.removeClass('k-dsd-arrow-up');
2904         sigicon.removeClass('k-dsd-arrow-down');
2905         sigicon.removeClass('fa-arrow-up');
2906         sigicon.removeClass('fa-arrow-down');
2907
2908         signal = data['kismet.device.base.signal']['kismet.common.signal.last_signal'];
2909
2910         if (signal < lastsignal) {
2911             sigicon.addClass('k-dsd-arrow-down');
2912             sigicon.addClass('fa-arrow-down');
2913             sigicon.show();
2914         } else {
2915             sigicon.addClass('k-dsd-arrow-up');
2916             sigicon.addClass('fa-arrow-up');
2917             sigicon.show();
2918         }
2919
2920         var typestr = "";
2921         if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "dbm")
2922             typestr = " dBm";
2923         else if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "rssi") 
2924             typestr = " RSSI";
2925
2926         $('.k-dsd-lastsignal', devsignal_panel.content)
2927             .text(signal + typestr);
2928
2929         $('.k-dsd-minsignal', devsignal_panel.content)
2930         .text(data['kismet.device.base.signal']['kismet.common.signal.min_signal'] + typestr);
2931
2932         $('.k-dsd-maxsignal', devsignal_panel.content)
2933         .text(data['kismet.device.base.signal']['kismet.common.signal.max_signal'] + typestr);
2934
2935         // Common point titles
2936         var pointtitles = new Array();
2937
2938         for (var x = 60; x > 0; x--) {
2939             if (x % 5 == 0) {
2940                 pointtitles.push(x + 's');
2941             } else {
2942                 pointtitles.push(' ');
2943             }
2944         }
2945
2946
2947         /*
2948         var rrdata = kismet.RecalcRrdData(
2949             data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2950             data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2951             kismet.RRD_SECOND,
2952             data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.minute_vec'], {});
2953
2954         // We assume the 'best' a signal can usefully be is -20dbm,
2955         // that means we're right on top of it.
2956         // We can assume that -100dbm is a sane floor value for
2957         // the weakest signal.
2958         // If a signal is 0 it means we haven't seen it at all so
2959         // just ignore that data point
2960         // We turn signals into a 'useful' graph by clamping to
2961         // -100 and -20 and then scaling it as a positive number.
2962         var moddata = new Array();
2963
2964         for (var x = 0; x < rrdata.length; x++) {
2965             var d = rrdata[x];
2966
2967             if (d == 0) {
2968                 moddata.push(0);
2969                 continue;
2970             }
2971
2972             if (d < -100)
2973                 d = -100;
2974
2975             if (d > -20)
2976                 d = -20;
2977
2978             // Normalize to 0-80
2979             d = (d * -1) - 20;
2980
2981             // Reverse (weaker is worse), get as percentage
2982             var rs = (80 - d) / 80;
2983
2984             moddata.push(100*rs);
2985         }
2986         */
2987
2988         var msignal = signal;
2989
2990         if (msignal == 0) {
2991             fakerrd.push(0);
2992         } else if (msignal < -100) {
2993             msignal = -100;
2994         } else if (msignal > -20) {
2995             msignal = -20;
2996         }
2997
2998         msignal = (msignal * -1) - 20;
2999         var rs = (80 - msignal) / 80;
3000
3001         fakerrd.push(100 * rs);
3002
3003         fakerrd.splice(0, 1);
3004
3005         var moddata = fakerrd;
3006
3007         var datasets = [
3008             {
3009                 label: 'Signal (%)',
3010                 fill: 'false',
3011                 borderColor: 'blue',
3012                 backgroundColor: 'rgba(100, 100, 255, 0.83)',
3013                 data: moddata,
3014             },
3015         ];
3016
3017         if (devsignal_chart == null) {
3018             var canvas = $('#k-dsd-canvas', devsignal_panel.content);
3019
3020             devsignal_chart = new Chart(canvas, {
3021                 type: 'bar',
3022                 options: {
3023                     responsive: true,
3024                     maintainAspectRatio: false,
3025                     animation: false,
3026                     scales: {
3027                         yAxes: [ {
3028                             ticks: {
3029                                 beginAtZero: true,
3030                                 max: 100,
3031                             }
3032                         }],
3033                     },
3034                 },
3035                 data: {
3036                     labels: pointtitles,
3037                     datasets: datasets
3038                 }
3039             });
3040         } else {
3041             devsignal_chart.data.datasets[0].data = moddata;
3042             devsignal_chart.update();
3043         }
3044
3045
3046     })
3047     .always(function() {
3048         devsignal_tid = setTimeout(function() {
3049                 devsignal_refresh(key, devsignal_panel,
3050                     devsignal_chart, devsignal_tid, signal, fakerrd);
3051         }, 1000);
3052     });
3053 };
3054
3055 exports.login_error = false;
3056 exports.login_pending = false;
3057
3058 exports.ProvisionedPasswordCheck = function(cb) {
3059     $.ajax({
3060         url: local_uri_prefix + "session/check_setup_ok",
3061
3062         error: function(jqXHR, textStatus, errorThrown) {
3063             cb(jqXHR.status);
3064         },
3065
3066         success: function(data, textStatus, jqHXR) {
3067             cb(200);
3068         },
3069     });
3070 }
3071
3072 exports.LoginCheck = function(cb, user, pw) {
3073     user = user || kismet.getStorage('kismet.base.login.username', 'kismet');
3074     pw = pw || kismet.getStorage('kismet.base.login.password', '');
3075
3076     $.ajax({
3077         url: local_uri_prefix + "session/check_login",
3078
3079         beforeSend: function (xhr) {
3080             xhr.setRequestHeader ("Authorization", "Basic " + btoa(user + ":" + pw));
3081         },
3082
3083         xhrFields: {
3084             withCredentials: false
3085         },
3086
3087         error: function(jqXHR, textStatus, errorThrown) {
3088             cb(false);
3089         },
3090
3091         success: function(data, textStatus, jqXHR) {
3092             cb(true);
3093         }
3094
3095     });
3096 }
3097
3098 exports.FirstLoginCheck = function(first_login_done_cb) {
3099     var loginpanel = null; 
3100     var username_deferred = $.Deferred();
3101
3102     $.get(local_uri_prefix + "system/user_status.json")
3103         .done(function(data) {
3104             username_deferred.resolve(data['kismet.system.user']);
3105         })
3106         .fail(function() {
3107             username_deferred.resolve("[unknown]");
3108         });
3109
3110     var username = "[incomplete]";
3111     $.when(username_deferred).done(function(v) {
3112
3113     username = v;
3114
3115     var required_login_content = 
3116     $('<div>', {
3117         style: 'padding: 10px;'
3118     })
3119     .append(
3120         $('<p>')
3121         .html('Kismet requires a login to access data.')
3122     )
3123     .append(
3124         $('<p>')
3125         .html('Your login is stored in in <code>.kismet/kismet_httpd.conf</code> in the <i>home directory of the user who launched Kismet</i>;  This server is running as ' + username + ', and the login will be saved in <code>~' + username + '/.kismet/kismet_httpd.conf</code>.')
3126     )
3127     .append(
3128         $('<div>', {
3129             id: 'form'
3130         })
3131         .append(
3132             $('<fieldset>', {
3133                 id: 'fs_login'
3134             })
3135             .append(
3136                 $('<span style="display: inline-block; width: 8em;">')
3137                 .html('User name: ')
3138             )
3139             .append(
3140                 $('<input>', {
3141                     type: 'text',
3142                     name: 'user',
3143                     id: 'req_user'
3144                 })
3145             )
3146             .append(
3147                 $('<br>')
3148             )
3149             .append(
3150                 $('<span style="display: inline-block; width: 8em;">')
3151                 .html('Password: ')
3152             )
3153             .append(
3154                 $('<input>', {
3155                     type: 'password',
3156                     name: 'password',
3157                     id: 'req_password'
3158                 })
3159             )
3160             .append(
3161                 $('<div>', {
3162                     style: 'padding-top: 10px;'
3163                 })
3164                 .append(
3165                     $('<button>', {
3166                         class: 'k-wl-button-close',
3167                         id: 'login_button',
3168                     })
3169                     .text('Log in')
3170                     .button()
3171                 )
3172                 .append(
3173                     $('<span>', {
3174                         id: 'pwsuccessdiv',
3175                         style: 'padding-left: 5px',
3176                     })
3177                     .append(
3178                         $('<i>', {
3179                             id: 'pwsuccess',
3180                             class: 'fa fa-refresh fa-spin',
3181                         })
3182                     )
3183                     .append(
3184                         $('<span>', {
3185                             id: 'pwsuccesstext'
3186                         })
3187                     )
3188                     .hide()
3189                 )
3190             )
3191         )
3192     );
3193
3194     var login_checker_cb = function(content) {
3195         var savebutton = $('#login_button', content);
3196
3197         var checkerdiv = $('#pwsuccessdiv', content);
3198         var checker = $('#pwsuccess', checkerdiv);
3199         var checkertext = $('#pwsuccesstext', checkerdiv);
3200
3201         checker.removeClass('fa-exclamation-circle');
3202         checker.removeClass('fa-check-square');
3203
3204         checker.addClass('fa-spin');
3205         checker.addClass('fa-refresh');
3206         checkertext.text("  Checking...");
3207
3208         checkerdiv.show();
3209
3210         exports.LoginCheck(function(success) {
3211             if (!success) {
3212                 checker.removeClass('fa-check-square');
3213                 checker.removeClass('fa-spin');
3214                 checker.removeClass('fa-refresh');
3215                 checker.addClass('fa-exclamation-circle');
3216                 checkertext.text("  Invalid login");
3217             } else {
3218                 /* Save the login info */
3219                 kismet.putStorage('kismet.base.login.username', $('#req_user', content).val());
3220                 kismet.putStorage('kismet.base.login.password', $('#req_password', content).val());
3221
3222                 loginpanel.close();
3223
3224                 /* Call the primary callback */
3225                 first_login_done_cb();
3226             }
3227         }, $('#req_user', content).val(), $('#req_password', content).val());
3228     };
3229
3230     $('#login_button', required_login_content)
3231         .button()
3232         .on('click', function() {
3233             login_checker_cb(required_login_content);
3234         });
3235
3236     $('fs_login', required_login_content).controlgroup();
3237
3238     var set_password_content = 
3239     $('<div>', {
3240         style: 'padding: 10px;'
3241     })
3242     .append(
3243         $('<p>')
3244         .html('To finish setting up Kismet, you need to configure a login.')
3245     )
3246     .append(
3247         $('<p>')
3248         .html('This login will be stored in <code>.kismet/kismet_httpd.conf</code> in the <i>home directory of the user who launched Kismet</i>;  This server is running as ' + username + ', and the login will be saved in <code>~' + username + '/.kismet/kismet_httpd.conf</code>.')
3249     )
3250     .append(
3251         $('<div>', {
3252             id: 'form'
3253         })
3254         .append(
3255             $('<fieldset>', {
3256                 id: 'fs_login'
3257             })
3258             .append(
3259                 $('<legend>', {})
3260                 .html('Set Login')
3261             )
3262             .append(
3263                 $('<span style="display: inline-block; width: 8em;">')
3264                 .html('User name: ')
3265             )
3266             .append(
3267                 $('<input>', {
3268                     type: 'text',
3269                     name: 'user',
3270                     id: 'user'
3271                 })
3272             )
3273             .append(
3274                 $('<br>')
3275             )
3276             .append(
3277                 $('<span style="display: inline-block; width: 8em;">')
3278                 .html('Password: ')
3279             )
3280             .append(
3281                 $('<input>', {
3282                     type: 'password',
3283                     name: 'password',
3284                     id: 'password'
3285                 })
3286             )
3287             .append(
3288                 $('<br>')
3289             )
3290             .append(
3291                 $('<span style="display: inline-block; width: 8em;">')
3292                 .html('Confirm: ')
3293             )
3294             .append(
3295                 $('<input>', {
3296                     type: 'password',
3297                     name: 'password2',
3298                     id: 'password2'
3299                 })
3300             )
3301             .append(
3302                 $('<span>', {
3303                     id: 'pwsuccessdiv',
3304                     style: 'padding-left: 5px',
3305                 })
3306                 .append(
3307                     $('<i>', {
3308                         id: 'pwsuccess',
3309                         class: 'fa fa-refresh fa-spin',
3310                     })
3311                 )
3312                 .append(
3313                     $('<span>', {
3314                         id: 'pwsuccesstext'
3315                     })
3316                 )
3317                 .hide()
3318             )
3319         )
3320     )
3321     .append(
3322         $('<div>', {
3323             style: 'padding-top: 10px;'
3324         })
3325         .append(
3326             $('<button>', {
3327                 class: 'k-wl-button-close',
3328                 id: 'save_password',
3329             })
3330             .text('Save')
3331             .button()
3332         )
3333     );
3334
3335     var checker_cb = function(content) {
3336         var savebutton = $('#save_password', content);
3337         var checkerdiv = $('#pwsuccessdiv', content);
3338         var checker = $('#pwsuccess', checkerdiv);
3339         var checkertext = $('#pwsuccesstext', checkerdiv);
3340
3341         savebutton.button("disable");
3342
3343         checker.removeClass('fa-exclamation-circle');
3344         checker.removeClass('fa-check-square');
3345
3346         checker.addClass('fa-spin');
3347         checker.addClass('fa-refresh');
3348         checkertext.text("");
3349
3350         checkerdiv.show();
3351
3352         if ($('#user', content).val().length == 0) {
3353             checker.removeClass('fa-check-square');
3354             checker.removeClass('fa-spin');
3355             checker.removeClass('fa-refresh');
3356             checker.addClass('fa-exclamation-circle');
3357             checkertext.text("  Username required");
3358             savebutton.button("disable");
3359             return;
3360         }
3361
3362         if ($('#password', content).val().length == 0) {
3363             checker.removeClass('fa-check-square');
3364             checker.removeClass('fa-spin');
3365             checker.removeClass('fa-refresh');
3366             checker.addClass('fa-exclamation-circle');
3367             checkertext.text("  Password required");
3368             savebutton.button("disable");
3369             return;
3370         }
3371
3372         if ($('#password', content).val() != $('#password2', content).val()) {
3373             checker.removeClass('fa-check-square');
3374             checker.removeClass('fa-spin');
3375             checker.removeClass('fa-refresh');
3376             checker.addClass('fa-exclamation-circle');
3377             checkertext.text("  Passwords don't match");
3378             savebutton.button("disable");
3379             return;
3380         }
3381
3382         checker.removeClass('fa-exclamation-circle');
3383         checker.removeClass('fa-spin');
3384         checker.removeClass('fa-refresh');
3385         checker.addClass('fa-check-square');
3386         checkertext.text("");
3387         savebutton.button("enable");
3388
3389     };
3390
3391     jQuery('#user', set_password_content).on('input propertychange paste', function() {
3392         checker_cb();
3393     });
3394     jQuery('#password', set_password_content).on('input propertychange paste', function() {
3395         checker_cb();
3396     });
3397     jQuery('#password2', set_password_content).on('input propertychange paste', function() {
3398         checker_cb();
3399     });
3400
3401     $('#save_password', set_password_content)
3402         .button()
3403         .on('click', function() {
3404             kismet.putStorage('kismet.base.login.username', $('#user', set_password_content).val());
3405             kismet.putStorage('kismet.base.login.password', $('#password', set_password_content).val());
3406
3407             var postdata = {
3408                 "username": $('#user', set_password_content).val(),
3409                 "password": $('#password', set_password_content).val()
3410             };
3411
3412             $.ajax({
3413                 type: "POST",
3414                 url: local_uri_prefix + "session/set_password",
3415                 data: postdata,
3416                 error: function(jqXHR, textStatus, errorThrown) {
3417                     alert("Could not set login, check your kismet server logs.")
3418                 },
3419             });
3420
3421             loginpanel.close();
3422
3423             /* Call the primary callback to load the UI */
3424             first_login_done_cb();
3425
3426             /* Check for the first-time running */
3427             exports.FirstTimeCheck();
3428         });
3429
3430     $('fs_login', set_password_content).controlgroup();
3431
3432     checker_cb(set_password_content);
3433
3434     var w = ($(window).width() / 2) - 5;
3435     if (w < 450) {
3436         w = $(window).width() - 5;
3437     }
3438
3439     var content = set_password_content;
3440
3441     exports.ProvisionedPasswordCheck(function(code) {
3442         if (code == 200 || code == 406) {
3443             /* Initial setup has been complete, now check the login itself */
3444             exports.LoginCheck(function(success) {
3445                 if (!success) {
3446                     loginpanel = $.jsPanel({
3447                         id: "login-alert",
3448                         headerTitle: '<i class="fa fa-exclamation-triangle"></i>Login Required',
3449                         headerControls: {
3450                             controls: 'closeonly',
3451                             iconfont: 'jsglyph',
3452                         },
3453                         contentSize: w + " auto",
3454                         paneltype: 'modal',
3455                         content: required_login_content,
3456                     });
3457
3458                     return true;
3459                 } else {
3460                     /* Otherwise we're all good, continue to loading the main UI via the callback */
3461                     first_login_done_cb();
3462                 }
3463             });
3464         } else if (code == 500) {
3465             loginpanel = $.jsPanel({
3466                 id: "login-alert",
3467                 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Set Login',
3468                 headerControls: {
3469                     controls: 'closeonly',
3470                     iconfont: 'jsglyph',
3471                 },
3472                 contentSize: w + " auto",
3473                 paneltype: 'modal',
3474                 content: set_password_content,
3475             });
3476
3477             return true;
3478         } else {
3479             loginpanel = $.jsPanel({
3480                 id: "login-alert",
3481                 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Error connecting',
3482                 headerControls: {
3483                     controls: 'closeonly',
3484                     iconfont: 'jsglyph',
3485                 },
3486                 contentSize: w + " auto",
3487                 paneltype: 'modal',
3488                 content: "Error connecting to Kismet and checking provisioning; try reloading the page!",
3489             });
3490
3491
3492         }
3493    });
3494
3495    // When clause
3496    });
3497 }
3498
3499 exports.FirstTimeCheck = function() {
3500     var welcomepanel = null; 
3501     if (kismet.getStorage('kismet.base.seen_welcome', false) == false) {
3502         var content = 
3503             $('<div>', {
3504                 style: 'padding: 10px;'
3505             })
3506             .append(
3507                 $('<p>', { }
3508                 )
3509                 .html("Welcome!")
3510             )
3511             .append(
3512                 $('<p>')
3513                 .html('This is the first time you\'ve used this Kismet server in this browser.')
3514             )
3515             .append(
3516                 $('<p>')
3517                 .html('Kismet stores local settings in the HTML5 storage of your browser.')
3518             )
3519             .append(
3520                 $('<p>')
3521                 .html('You should configure your preferences and login settings in the settings panel!')
3522             )
3523             .append(
3524                 $('<div>', {})
3525                 .append(
3526                     $('<button>', {
3527                         class: 'k-w-button-settings'
3528                     })
3529                     .text('Settings')
3530                     .button()
3531                     .on('click', function() {
3532                         welcomepanel.close();               
3533                         kismet_ui_settings.ShowSettings();
3534                     })
3535                 )
3536                 .append(
3537                     $('<button>', {
3538                         class: 'k-w-button-close',
3539                         style: 'position: absolute; right: 5px;',
3540                     })
3541                     .text('Continue')
3542                     .button()
3543                     .on('click', function() {
3544                         welcomepanel.close();
3545                     })
3546                 )
3547
3548             );
3549
3550         welcomepanel = $.jsPanel({
3551             id: "welcome-alert",
3552             headerTitle: '<i class="fa fa-power-off"></i> Welcome',
3553             headerControls: {
3554                 controls: 'closeonly',
3555                 iconfont: 'jsglyph',
3556             },
3557             contentSize: "auto auto",
3558             paneltype: 'modal',
3559             content: content,
3560         });
3561
3562         kismet.putStorage('kismet.base.seen_welcome', true);
3563
3564         return true;
3565     }
3566
3567     return false;
3568 }
3569
3570 // Keep trying to fetch the servername until we're able to
3571 var servername_tid = -1;
3572 exports.FetchServerName = function(cb) {
3573     $.get(local_uri_prefix + "system/status.json")
3574         .done(function (d) {
3575             d = kismet.sanitizeObject(d);
3576             cb(d['kismet.system.server_name']);
3577         })
3578         .fail(function () {
3579             servername_tid = setTimeout(function () {
3580                 exports.FetchServerName(cb);
3581             }, 1000);
3582         });
3583 }
3584
3585 /* Highlight active devices */
3586 kismet_ui.AddDeviceRowHighlight({
3587     name: "Active",
3588     description: "Device has been active in the past 10 seconds",
3589     priority: 500,
3590     defaultcolor: "#cee1ff",
3591     defaultenable: false,
3592     fields: [
3593         'kismet.device.base.last_time'
3594     ],
3595     selector: function(data) {
3596         var ts = data['kismet.device.base.last_time'];
3597
3598         return (kismet.timestamp_sec - ts < 10);
3599     }
3600 });
3601
3602 /* Bodycam hardware of various types */
3603 kismet_ui.AddDeviceRowHighlight({
3604     name: "Bodycams",
3605     description: "Body camera devices",
3606     priority: 500,
3607     defaultcolor: "#0089FF",
3608     defaultenable: true,
3609     fields: [
3610         'kismet.device.base.macaddr',
3611         'kismet.device.base.commonname',
3612     ],
3613     selector: function(data) {
3614         try {
3615             if (data['kismet.device.base.macaddr'].match("^00:25:DF") != null)
3616                 return true;
3617             if (data['kismet.device.base.macaddr'].match("^12:20:13") != null)
3618                 return true;
3619             if (data['kismet.device.base.common_name'].match("^Axon-X") != null)
3620                 return true;
3621         } catch (e) {
3622             return false;
3623         }
3624
3625         return false;
3626     }
3627 });
3628
3629 // We're done loading
3630 exports.load_complete = 1;
3631
3632 return exports;
3633
3634 });