diff --git a/RELEASE NOTES.md b/RELEASE NOTES.md index cc9214ad..58d1403b 100644 --- a/RELEASE NOTES.md +++ b/RELEASE NOTES.md @@ -66,4 +66,21 @@ 1. Programmable delay for sending keystrokes to webpage link screen instead of fixed 10 seconds - under weblink +**2.5.4** + +1. Inline with pisignage.com release 2.5.4 (**a8a234f96**) + - Media RSS, video/image text messages, asset specific ticker message, zone4 video support, mute support for video streaming + - Support for Pi 4 and 4K assets + - Support for start and end hour for asset validity period + - Local folder/file play support + - Support shuffle content before play and merge alternate assets from different playlists options for group playlists +1. Added dashboard +1. Remove deprecate warnings and server crashes +1. Default resolution of new groups changed to auto +1. Display CPU temperature both in Centigrade and Fahrenheit +1. Auto adjustment of end-date when start-date is changed to the start date (if end date is earlier) +1. Limits enhanced - file size for upload to 5GB, max files to 100, max playlists to schedule to 100 +1. Issue fixes with UI +1. Lexicographic sorting of assets and playlists + diff --git a/app/controllers/players.js b/app/controllers/players.js index 1b28a80b..914bb227 100644 --- a/app/controllers/players.js +++ b/app/controllers/players.js @@ -165,6 +165,8 @@ var sendConfig = function (player, group, periodic) { retObj.emergencyMessage = group.emergencyMessage || {enable: false}; retObj.combineDefaultPlaylist = group.combineDefaultPlaylist || false; retObj.playAllEligiblePlaylists = group.playAllEligiblePlaylists || false; + retObj.shuffleContent = group.shuffleContent || false; + retObj.alternateContent = group.alternateContent || false; retObj.urlReloadDisable = group.urlReloadDisable || false; retObj.loadPlaylistOnCompletion = group.loadPlaylistOnCompletion || false; //if (!pipkgjson) diff --git a/app/controllers/scheduler.js b/app/controllers/scheduler.js index 489df1b5..79aec952 100644 --- a/app/controllers/scheduler.js +++ b/app/controllers/scheduler.js @@ -71,9 +71,10 @@ var checkAndDownloadImage = function() { //read version, different from local one if (!update) { fs.access(path.join(config.releasesDir,"piimage"+serverVersion+"-v6.zip"), (fs.constants || fs).F_OK, function(err) { - if (err) + if (err) { update = true; - console.log(err); + console.log(err); + } async_cb(err) }); } else { diff --git a/package.json b/package.json index 08bb7495..76b558fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pisignage-server", - "version": "2.3.0", + "version": "2.5.4", "description": "Server to manage piSignage players in a LAN or Private Network.", "author": "colloqi ", "engines": { diff --git a/public/app/css/custom.css b/public/app/css/custom.css index d4d28c80..a3e15f63 100644 --- a/public/app/css/custom.css +++ b/public/app/css/custom.css @@ -97,7 +97,24 @@ body { .text-lightgreen { color:lightgreen; } - +.success{ + background: #00C993; +} +.primary{ + background: #01A9D8; +} +.info{ + background: #A088F6; +} +.warning{ + background: #E4D757; +} +.light-danger{ + background: #EEA1A9; +} +.danger{ + background: #EB4559; +} .pl-5 { padding-left: 5px; } @@ -127,6 +144,87 @@ body { max-width:988px; transform: rotate(90deg); } +.statcard-number { + margin-top: 0; + margin-bottom: 0; +} +.statcard-number .label { + padding: .125em .5em; + font-size: 50%; + vertical-align: middle; +} +.statcard-desc { + font-size: 85%; + letter-spacing: .15em; + color: #888888; + text-transform: uppercase; +} + +.statcard-primary, +.statcard-success, +.statcard-info, +.statcard-warning, +.statcard-danger { + color: #fff; + border-radius: 3px; +} +.statcard-primary .delta-negative, +.statcard-success .delta-negative, +.statcard-info .delta-negative, +.statcard-warning .delta-negative, +.statcard-danger .delta-negative, +.statcard-primary .delta-positive, +.statcard-success .delta-positive, +.statcard-info .delta-positive, +.statcard-warning .delta-positive, +.statcard-danger .delta-positive, +.statcard-primary .statcard-number, +.statcard-success .statcard-number, +.statcard-info .statcard-number, +.statcard-warning .statcard-number, +.statcard-danger .statcard-number { + font-weight: normal; + color: inherit; +} +.statcard-primary .statcard-desc, +.statcard-success .statcard-desc, +.statcard-info .statcard-desc, +.statcard-warning .statcard-desc, +.statcard-danger .statcard-desc { + font-weight: normal; + color: rgba(255, 255, 255, 0.65); +} +.statcard-primary .statcard-hr, +.statcard-success .statcard-hr, +.statcard-info .statcard-hr, +.statcard-warning .statcard-hr, +.statcard-danger .statcard-hr { + margin-left: -20px; + margin-right: -20px; + border-top-color: rgba(255, 255, 255, 0.2); +} +.statcard-primary { + background-color: #1CA8DD; +} +.statcard-success { + background-color: #1BC98E; +} +.statcard-info { + background-color: #9F86FF; +} +.statcard-warning { + background-color: #E4D836; +} +.statcard-danger { + background-color: #E64759; +} + +.statcard-light-danger { + background-color:#eba1a9; +} + + + diff --git a/public/app/js/app-pc.js b/public/app/js/app-pc.js index 714e49cc..356b7ca9 100644 --- a/public/app/js/app-pc.js +++ b/public/app/js/app-pc.js @@ -9,6 +9,7 @@ angular.module('piServerApp', [ 'piConfig', 'piIndex.controllers', 'piGroups.controllers', + 'dashboard.controllers', 'piAssets.controllers', 'piAssets.services', 'piPlayers.services', @@ -61,6 +62,26 @@ angular.module('piServerApp', [ } }) + .state("home.dashboard",{ + url: "dashboard", + views: { + "main": { + templateUrl: '/app/partials/dashboard.html', + controller: 'DashboardCtrl' + } + } + }) + + .state("home.dashboard_players", { + url: "dashboard/players?groupName&locationName¤tPlaylist&version&bucket", + views: { + "main": { + templateUrl: '/app/partials/players.html', + controller:'ServerPlayerCtrl' + } + } + }) + // .state("home.players.players_details", { // url: "players/:player?group", // views: { diff --git a/public/app/js/controllers/dashboard.js b/public/app/js/controllers/dashboard.js new file mode 100644 index 00000000..58696aa3 --- /dev/null +++ b/public/app/js/controllers/dashboard.js @@ -0,0 +1,92 @@ +'use strict'; + +angular.module('dashboard.controllers', []) + .controller('DashboardCtrl', function ($scope,$http,$interval,piUrls) { + + var BUCKET_INTERVALS = [5,60,240,24 * 60, 7 * 24 * 60]; + + $scope.COUNT_FIELDS = [ + {field:"groupName",name:"Group wise"}, + {field:"currentPlaylist",name:"Playlists playing"}, + {field:"version",name:"Software version"}, + {field:"locationName",name:"Location wise"} + ] + + $scope.playersStatFieldWise = {} + + $scope.BUCKET_TITLE = ["now","Last 60 minutes","Last 4 hours","Today","Last 7 days","> 7 days"] + $scope.BUCKET_CLASS = ["success","primary","info","warning","light-danger","danger"] + + var getPlayers = function() { + var options = {params: {}}, + lastReportedTimeInMinutes; + + $http.get(piUrls.players, options) + .then(function(response) { + var data = response.data; + if (data.success) { + $scope.players = data.data.objects; + $scope.currentVersion = data.data.currentVersion; + + $scope.COUNT_FIELDS.forEach(function(obj){ + $scope.playersStatFieldWise[obj.field] = {} + }) + $scope.playersStat = [0,0,0,0,0,0] + $scope.playersExpectedToReport = [] + + $scope.players.forEach(function(player){ + var l, lIndex; + + player.lastReported = player.lastReported || 0; //never reported + player.groupName = player.group && player.group.name + player.locationName = player.configLocation || player.location + + $scope.COUNT_FIELDS.forEach(function(obj){ + l = player[obj.field] || "NA"; + if (l && l.trim()) { + l = l.trim() + if (!$scope.playersStatFieldWise[obj.field][l]) + $scope.playersStatFieldWise[obj.field][l] = {name: l, count: 1} + else + $scope.playersStatFieldWise[obj.field][l].count += 1; + } + }) + + lastReportedTimeInMinutes = parseInt((Date.now() - (new Date(player.lastReported).getTime()))/60000); + for (var i=0,len=BUCKET_INTERVALS.length;i 6 && lastReportedTimeInMinutes < 60) + $scope.playersExpectedToReport.push({name:player.name || player.localName || ("Player "+player.cpuSerialNumber.slice(12)),lastReported:player.lastReported}) + + }); + $scope.COUNT_FIELDS.forEach(function(obj){ + $scope.playersStatFieldWise[obj.field+"Count"] = [] + for(var objectKey in $scope.playersStatFieldWise[obj.field]) { + $scope.playersStatFieldWise[obj.field+"Count"].push($scope.playersStatFieldWise[obj.field][objectKey]); + } + + $scope.playersStatFieldWise[obj.field+"Count"].sort(function(a, b){ + return parseInt(b.count) - parseInt(a.count); + }); + }) + } + $scope.COUNT_FIELDS_TO_SHOW = $scope.COUNT_FIELDS; + + },function (response) { + }); + } + getPlayers() + $scope.playerFetchTimer =$interval(getPlayers,60000); + + getPlayers(); + + $scope.$on("$destroy", function(){ + $interval.cancel($scope.playerFetchTimer) + }); + }) diff --git a/public/app/js/controllers/groups.js b/public/app/js/controllers/groups.js index 07d98de0..9b5a4d8c 100644 --- a/public/app/js/controllers/groups.js +++ b/public/app/js/controllers/groups.js @@ -338,6 +338,17 @@ angular.module('piGroups.controllers', []) if ($scope.forPlaylist.settings.enddate) { $scope.forPlaylist.settings.enddate = new Date($scope.forPlaylist.settings.enddate) } + $scope.today = new Date().toISOString().split("T")[0]; + $scope.$watch("forPlaylist.settings.startdate", function(value) { + if (value) { + var endday = new Date(value); + $scope.endday = endday.toISOString().split("T")[0]; + if (!$scope.forPlaylist.settings.enddate || + value > $scope.forPlaylist.settings.enddate) + $scope.forPlaylist.settings.enddate = endday; + } + }); + // if ($scope.forPlaylist.settings.starttimeObj) { // $scope.forPlaylist.settings.starttimeObj = new Date($scope.forPlaylist.settings.starttimeObj) // } diff --git a/public/app/js/services/players.js b/public/app/js/services/players.js index b5edf224..a70b254a 100644 --- a/public/app/js/services/players.js +++ b/public/app/js/services/players.js @@ -1,12 +1,17 @@ 'use strict'; - /* */ angular.module('piPlayers.services', []) - .factory('playerLoader', function ($http,piUrls,$state,assetLoader) { + .factory('playerLoader', function ($http,piUrls,$state,assetLoader,$stateParams) { var observerCallbacks = {}; + var BUCKET_INTERVALS = [0,5,60,240,24 * 60, 7 * 24 * 60,Infinity]; + var filterByBucket = function(playerReportedTime) { + playerReportedTime = playerReportedTime || 0; + var lastReportedTimeInMinutes = parseInt((Date.now() - (new Date(playerReportedTime).getTime()))/60000); + return ((lastReportedTimeInMinutes < BUCKET_INTERVALS[parseInt($stateParams.bucket)]) || (lastReportedTimeInMinutes > BUCKET_INTERVALS[parseInt($stateParams.bucket)+1])); + } var notifyObservers = function(){ angular.forEach(observerCallbacks, function(callback){ callback(); @@ -30,9 +35,34 @@ angular.module('piPlayers.services', []) $http.get(piUrls.players, options) .success(function (data, status) { if (data.success) { + var bucketIndex = parseInt($stateParams.bucket), + playlistPlaying=$stateParams.currentPlaylist, + groupWise=$stateParams.groupName, + versionWise=$stateParams.version, + locationWise=$stateParams.locationName; playerLoader.player.players = data.data.objects; playerLoader.player.currentVersion = data.data.currentVersion; playerLoader.player.players.forEach(function(player){ + if (!isNaN(bucketIndex)) { + //filter players + for (var i=playerLoader.player.players.length -1;i>=0;i--) { + var filter = filterByBucket(playerLoader.player.players[i].lastReported) + if (filter) + playerLoader.player.players.splice(i,1); + } + } + if(playlistPlaying) + playerLoader.player.players= playerLoader.player.players.filter(function(player) {return (player.currentPlaylist==playlistPlaying)}); + + if(groupWise) + playerLoader.player.players=playerLoader.player.players.filter(function(player) {return (player.group.name==groupWise)}); + + if(versionWise) + playerLoader.player.players=playerLoader.player.players.filter(function(player) {return (player.version==versionWise)}); + + if(locationWise) + playerLoader.player.players=playerLoader.player.players.filter(function(player) {return (player.locationName==locationWise)}); + if (!player.isConnected) player.statusClass = "text-danger" else if (!player.playlistOn) @@ -53,6 +83,10 @@ angular.module('piPlayers.services', []) } else { player.uptimeFormatted = ""; } + if (player.piTemperature) { + var f = parseInt(parseInt(player.piTemperature) * 9/5 +32) + player.piTemperature = player.piTemperature + "/" +f+"'F" + } if (!player.lastReported) @@ -62,6 +96,16 @@ angular.module('piPlayers.services', []) }) }); } + if(groupWise){ + playerLoader.player.players=playerLoader.player.players.filter( + function (player) { return player.group.name==groupWise}); + } + if(versionWise){ + playerLoader.player.players=playerLoader.player.players.filter(function (player) {return player.version===versionWise}); + } + if(locationWise){ + playerLoader.player.players=playerLoader.player.players.filter(function (player) {retrun ((player.locationName===locationWise)||"NA")}); + } cb(!data.success); }) .error(function (data, status) { diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html new file mode 100644 index 00000000..2fd4cf71 --- /dev/null +++ b/public/app/partials/dashboard.html @@ -0,0 +1,31 @@ + +
+
Player reporting status
+ +
Players expected to report now
+
+

{{player.name }}: {{player.lastReported | timeAgo}}

+
There are no players went offline just now
+
+
Players expected to report now
+ +
\ No newline at end of file diff --git a/public/app/partials/dashboard.jade b/public/app/partials/dashboard.jade new file mode 100644 index 00000000..673c6911 --- /dev/null +++ b/public/app/partials/dashboard.jade @@ -0,0 +1,31 @@ +.panel.panel-default + .panel-heading Player reporting status + .panel-body + .row.statcards + .col-sm-4.col-lg-2.m-b(ng-repeat='bucket in playersStat track by $index ') + .statcard(ng-class="'statcard-'+BUCKET_CLASS[$index]") + a.btn.btn-block(ng-href="{{'#/dashboard/players?bucket='+$index}}") + .p-a.text-white + span.statcard-desc {{BUCKET_TITLE[$index]}} + h2.statcard-number {{bucket}} + .panel-heading Players expected to report now + .panel-body(style="max-height:200px;overflow-y:scroll") + h4.text-center(ng-repeat="player in playersExpectedToReport | orderBy:'lastReported':true track by $index") {{player.name }}:  + i.text-muted {{player.lastReported | timeAgo}} + h6.text-center.text-muted(ng-show="playersExpectedToReport.length == 0") There are no players went offline just now + + .panel-heading Players expected to report now + .panel-body + .row + .col-md-6.m-b-md(ng-repeat-start="playerTable in COUNT_FIELDS_TO_SHOW" ) + .list-group + h4.list-group-item.list-group-item-info {{playerTable.name}} + a.list-group-item.text-primary(ng-repeat="row in playersStatFieldWise[playerTable.field+'Count'] | limitTo:(displayLimit || 10) track by $index", + ng-href="{{'#/dashboard/players?'+playerTable.field+'='+row.name}}") + | {{row.name}} + span.pull-right.text-muted {{row.count}} + a.btn.btn-primary-outline.p-x(ng-show="playersStatFieldWise[playerTable.field+'Count'].length > 10",ng-click="displayLimit==1000?displayLimit=10:displayLimit=1000;") + | {{displayLimit==1000?"Show less":"All players"}} + + .clearfix(ng-if="$index % 2 == 1") + div(ng-repeat-end) \ No newline at end of file diff --git a/public/app/partials/menu.html b/public/app/partials/menu.html index abde36ab..c50068fb 100644 --- a/public/app/partials/menu.html +++ b/public/app/partials/menu.html @@ -6,6 +6,7 @@