diff --git a/README.md b/README.md new file mode 100755 index 0000000..dee1710 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +StatusBoard +============= + +StatusBoard is a simple PHP web-based tool for displaying the status of services. Administrators can manually record incidents and provide estimated end times and simple descriptions. This tool is suitable for exposing status information to customers or other third parties and does not need to be connected to internal systems. + +Features +------- + +* Customisable list of Services and Sites. +* Manual reporting and status changes for Incidents. +* Multiple severity levels. +* Full admin UI. + +Requirements +------------ + +* PHP +* MYSQL +* Smarty +* sihnon-php-lib: https://github.com/optiz0r/sihnon-php-lib + diff --git a/build/schema/mysql.demo.sql b/build/schema/mysql.demo.sql new file mode 100644 index 0000000..73e82d8 --- /dev/null +++ b/build/schema/mysql.demo.sql @@ -0,0 +1,106 @@ +-- phpMyAdmin SQL Dump +-- version 3.1.4 +-- http://www.phpmyadmin.net +-- +-- Host: localhost:3306 +-- Generation Time: Dec 16, 2011 at 01:27 AM +-- Server version: 5.1.53 +-- PHP Version: 5.3.6-pl1-gentoo + +SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; + +-- +-- Database: `status-board` +-- + +-- -------------------------------------------------------- + +-- +-- Dumping data for table `settings` +-- + +UPDATE `settings` SET `value`='Example Status Board' WHERE `name`='site.title'; + +-- +-- Dumping data for table `service` +-- + +INSERT INTO `service` (`id`, `name`, `description`) VALUES +(1, 'Internet', 'Shared Internet connection.'), +(2, 'Web', 'Hosted web servers'), +(3, 'Email', 'Hosted email services'), +(4, 'DNS', 'Hosted DNS services'), +(5, 'LDAP', 'Hosted directory services'); + +-- +-- Dumping data for table `site` +-- + +INSERT INTO `site` (`id`, `service`, `name`, `description`) VALUES +(1, 1, 'Local', 'Local Internet access'), +(2, 2, 'Offsite', 'Offsite web services'), +(3, 3, 'Offsite', 'Offsite email services'), +(4, 4, 'Local', 'Primary DNS services'), +(5, 4, 'Offsite', 'Backup DNS services'), +(6, 5, 'Local', 'Local LDAP services'), +(7, 5, 'Offsite', 'Offsite LDAP services'); + +-- +-- Dumping data for table `incident` +-- + +INSERT INTO `incident` (`id`, `site`, `reference`, `description`, `start_time`, `estimated_end_time`, `actual_end_time`) VALUES +(1, 1, 'UK:0001', 'Intermittent packetloss on primary internet connection', 1324079805, 1324079805, NULL), +(2, 1, 'UK:0002', 'Full outage', 1324079805, 1324079805, NULL), +(3, 4, 'UK:0003', 'DNS zone maintenance', 1324082411, 1324082411, NULL); + +-- +-- Dumping data for table `incidentstatus` +-- + +INSERT INTO `incidentstatus` (`id`, `incident`, `status`, `description`, `ctime`) VALUES +(1, 1, 2, 'Initial classification', 1324079864), +(2, 2, 4, 'Initial classification', 1324079864), +(3, 1, 3, 'Status upgraded due to increasing impact from the ongoing issue.', 1324080307), +(4, 3, 1, 'Initial classification', 1324082426); + +-- +-- Dumping data for table `user` +-- + +INSERT INTO `user` (`id`, `username`, `password`, `fullname`, `email`, `last_login`, `last_password_change`) VALUES +(2, 'guest', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', 'Guest', NULL, NULL, 1324211553); + +-- +-- Dumping data for table `group` +-- + +INSERT INTO `group` (`id`, `name`, `description`) VALUES +(2, 'readonly', 'Basic group with read only access to the status boards.'); + +-- +-- Dumping data for table `usergroup` +-- + +INSERT INTO `usergroup` (`id`, `user`, `group`, `added`) VALUES +(2, 2, 2, 1324211572); + +-- +-- Dumping data for table `permission` +-- + +INSERT INTO `permission` (`id`, `name`, `description`) VALUES +(2, 'Update Status Boards', 'Permission to add/edit/delete any service or site.'), +(3, 'Update Incidents', 'Permission to create and update the status of any incident.'), +(4, 'View Status Boards', 'Permission to view the status of all services and sites, and details of any incident.'); + +-- +-- Dumping data for table `grouppermission` +-- + +INSERT INTO `grouppermission` (`id`, `group`, `permission`, `added`) VALUES +(2, 1, 2, 1324211935), +(3, 1, 3, 1324211935), +(4, 1, 4, 1324211935), +(5, 2, 4, 1324211935); + diff --git a/build/schema/mysql.sql b/build/schema/mysql.sql index 28c3ceb..bf99125 100644 --- a/build/schema/mysql.sql +++ b/build/schema/mysql.sql @@ -18,11 +18,6 @@ SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; -- -- Table structure for table `settings` -- --- Creation: Sep 24, 2010 at 07:22 PM --- Last update: Dec 04, 2011 at 01:19 PM --- Last check: Aug 20, 2011 at 10:32 PM --- - DROP TABLE IF EXISTS `settings`; CREATE TABLE IF NOT EXISTS `settings` ( `name` varchar(255) NOT NULL, @@ -34,10 +29,10 @@ CREATE TABLE IF NOT EXISTS `settings` ( -- -- Dumping data for table `settings` -- - INSERT INTO `settings` (`name`, `value`, `type`) VALUES ('debug.display_exceptions', '1', 'bool'), ('cache.base_dir', '/dev/shm/status-board/', 'string'), +('auth', 'Database', 'string'), ('logging.plugins', 'Database\nFlatFile', 'array(string)'), ('logging.Database', 'webui', 'array(string)'), ('logging.Database.webui.table', 'log', 'string'), @@ -48,14 +43,14 @@ INSERT INTO `settings` (`name`, `value`, `type`) VALUES ('logging.FlatFile.tmp.format', '%timestamp% %hostname%:%pid% %progname%:%file%[%line%] %message%', 'string'), ('logging.FlatFile.tmp.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'), ('logging.FlatFile.tmp.category', 'webui\ndefault', 'array(string)'), -('templates.tmp_path', '/var/tmp/status-board/', 'string'); +('templates.tmp_path', '/var/tmp/status-board/', 'string'), +('site.title', 'Status Board', 'string'), +('sessions', 1, 'bool'), +('sessions.path', '/', 'string'); -- -- Table structure for table `log` -- --- Creation: Aug 20, 2011 at 10:32 PM --- - DROP TABLE IF EXISTS `log`; CREATE TABLE IF NOT EXISTS `log` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -71,3 +66,307 @@ CREATE TABLE IF NOT EXISTS `log` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; +-- +-- Table structure for table `service` +-- +DROP TABLE IF EXISTS `service`; +CREATE TABLE IF NOT EXISTS `service` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL, + `description` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Table structure for table `service` +-- +DROP TABLE IF EXISTS `site`; +CREATE TABLE IF NOT EXISTS `site` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `service` int(10) unsigned NOT NULL, + `name` varchar(32) NOT NULL, + `description` text NOT NULL, + PRIMARY KEY (`id`), + KEY `service` (`service`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Table structure for table `incident` +-- +DROP TABLE IF EXISTS `incident`; +CREATE TABLE IF NOT EXISTS `incident` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `site` int(10) unsigned NOT NULL, + `reference` varchar(32) NOT NULL, + `description` text NOT NULL, + `start_time` int(10) NOT NULL, + `estimated_end_time` int(10) NULL, + `actual_end_time` int(10) NULL, + PRIMARY KEY (`id`), + KEY `site` (`site`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Table structure for table `incidentstatus` +-- +DROP TABLE IF EXISTS `incidentstatus`; +CREATE TABLE IF NOT EXISTS `incidentstatus` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `incident` int(10) unsigned NOT NULL, + `status` int(10) unsigned NOT NULL, + `description` text NOT NULL, + `ctime` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `incident` (`incident`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Table structure for view `incidentstatus_current_int` +-- +DROP VIEW IF EXISTS `incidentstatus_current_int`; +CREATE VIEW `incidentstatus_current_int` AS ( + SELECT + `incidentstatus`.`incident` AS `incident`, + MAX(`incidentstatus`.`id`) AS `latest` + FROM + `incidentstatus` + GROUP BY + `incidentstatus`.`incident` +); + +-- +-- Table structure for view `incidentstatus_current` +-- +DROP VIEW IF EXISTS `incidentstatus_current`; +CREATE VIEW `incidentstatus_current` AS ( + SELECT + `is`.`id` AS `id`, + `is`.`incident` AS `incident`, + `is`.`status` AS `status`, + `is`.`ctime` AS `ctime` + FROM ( + `incidentstatus` AS `is` + JOIN `incidentstatus_current_int` AS `isci` + ) + WHERE ( + (`isci`.`incident` = `is`.`incident`) + AND (`is`.`id` = `isci`.`latest`) + ) +); + +-- +-- Table structure for view `incident_open` +-- +DROP VIEW IF EXISTS `incident_open`; +CREATE VIEW `incident_open` AS ( + SELECT + `i`.*, + `isc`.`ctime` + FROM + `incident` AS `i` + JOIN `incidentstatus_current` AS `isc` + ON `i`.`id` = `isc`.`incident` + WHERE + `isc`.`status` IN (1,2,3,4) +); + +-- +-- Table structure for view `incident_closedtime` +-- +DROP VIEW IF EXISTS `incident_closedtime`; +CREATE VIEW `incident_closedtime` AS ( + SELECT + `incident` AS `incident`, + `ctime` AS `ctime` + FROM + `incidentstatus` + WHERE + `status` = 0 +); + +-- +-- Table structure for view `incident_opentimes` +-- +DROP VIEW IF EXISTS `incident_opentimes`; +CREATE VIEW `incident_opentimes` AS ( + SELECT + `i`.*, + IFNULL(`t`.`ctime`, 0xffffffff+0) AS `ctime` + FROM + `incident` as `i` + LEFT JOIN `incident_closedtime` AS `t` ON `i`.`id`=`t`.`incident` +); + +-- +-- Table structure for table `user` +-- +DROP TABLE IF EXISTS `user`; +CREATE TABLE IF NOT EXISTS `user` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(255) NOT NULL, + `password` char(40) NOT NULL, + `fullname` varchar(255) NULL, + `email` varchar(255) NULL, + `last_login` int(10) NULL, + `last_password_change` int(10) NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Dumping data for table `user` +-- +INSERT INTO `user` (`id`, `username`, `password`, `fullname`, `email`, `last_login`, `last_password_change`) VALUES +(1, 'admin', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', 'Administrator', NULL, NULL, 1324211456); + +-- +-- Table structure for table `group` +-- +DROP TABLE IF EXISTS `group`; +CREATE TABLE IF NOT EXISTS `group` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `description` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Dumping data for table `group` +-- +INSERT INTO `group` (`id`, `name`, `description`) VALUES +(1, 'admins', 'Administrative users will full control over the status boards.'); + +-- +-- Table structure for table `usergroup` +-- +DROP TABLE IF EXISTS `usergroup`; +CREATE TABLE IF NOT EXISTS `usergroup` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user` int(10) unsigned NOT NULL, + `group` int(10) unsigned NOT NULL, + `added` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `user` (`user`,`group`), + KEY `group` (`group`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Dumping data for table `usergroup` +-- +INSERT INTO `usergroup` (`id`, `user`, `group`, `added`) VALUES +(1, 1, 1, 1324211572); + +-- +-- Table structure for view `groups_by_user` +-- +DROP VIEW IF EXISTS `groups_by_user`; +CREATE VIEW `groups_by_user` AS ( + SELECT + `u`.`id` AS `user`, + `g`.* + FROM + `usergroup` as `ug` + LEFT JOIN `user` AS `u` ON `ug`.`user`=`u`.`id` + LEFT JOIN `group` AS `g` ON `ug`.`group`=`g`.`id` +); + +-- +-- Table structure for table `permission` +-- +DROP TABLE IF EXISTS `permission`; +CREATE TABLE IF NOT EXISTS `permission` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `description` text NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Dumping data for table `permission` +-- +INSERT INTO `permission` (`id`, `name`, `description`) VALUES +(1, 'Administrator', 'Full administrative rights.'); + + +-- +-- Table structure for table `grouppermission` +-- +DROP TABLE IF EXISTS `grouppermission`; +CREATE TABLE IF NOT EXISTS `grouppermission` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `group` int(10) unsigned NOT NULL, + `permission` int(10) unsigned NOT NULL, + `added` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `group` (`group`,`permission`), + KEY `permission` (`permission`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; + +-- +-- Dumping data for table `grouppermission` +-- +INSERT INTO `grouppermission` (`id`, `group`, `permission`, `added`) VALUES +(1, 1, 1, 1324211935); + +-- +-- Table structure for view `permissions_by_group` +-- +DROP VIEW IF EXISTS `permissions_by_group`; +CREATE VIEW `permissions_by_group` AS ( + SELECT + `g`.`id` AS `group`, + `p`.* + FROM + `grouppermission` as `gp` + LEFT JOIN `group` AS `g` ON `gp`.`group`=`g`.`id` + LEFT JOIN `permission` AS `p` on `gp`.`permission`=`p`.`id` +); + +-- +-- Table structure for view `permissions_by_user` +-- +DROP VIEW IF EXISTS `permissions_by_user`; +CREATE VIEW `permissions_by_user` AS ( + SELECT + `u`.`id` AS `user`, + `p`.* + FROM + `usergroup` as `ug` + LEFT JOIN `user` AS `u` ON `ug`.`user`=`u`.`id` + LEFT JOIN `permissions_by_group` AS `p` on `ug`.`group`=`p`.`group` +); + +-- +-- Constraints for dumped tables +-- + +-- +-- Constraints for table `grouppermission` +-- +ALTER TABLE `grouppermission` + ADD CONSTRAINT `grouppermission_ibfk_2` FOREIGN KEY (`permission`) REFERENCES `permission` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT `grouppermission_ibfk_1` FOREIGN KEY (`group`) REFERENCES `group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- +-- Constraints for table `incident` +-- +ALTER TABLE `incident` + ADD CONSTRAINT `incident_ibfk_1` FOREIGN KEY (`site`) REFERENCES `site` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- +-- Constraints for table `incidentstatus` +-- +ALTER TABLE `incidentstatus` + ADD CONSTRAINT `incidentstatus_ibfk_1` FOREIGN KEY (`incident`) REFERENCES `incident` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- +-- Constraints for table `site` +-- +ALTER TABLE `site` + ADD CONSTRAINT `site_ibfk_1` FOREIGN KEY (`service`) REFERENCES `service` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- +-- Constraints for table `usergroup` +-- +ALTER TABLE `usergroup` + ADD CONSTRAINT `usergroup_ibfk_2` FOREIGN KEY (`group`) REFERENCES `group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT `usergroup_ibfk_1` FOREIGN KEY (`user`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/public/images/Status_Icons/cross-circle.png b/public/images/Status_Icons/cross-circle.png new file mode 100755 index 0000000..20d6f5e Binary files /dev/null and b/public/images/Status_Icons/cross-circle.png differ diff --git a/public/images/Status_Icons/exclamation.png b/public/images/Status_Icons/exclamation.png new file mode 100755 index 0000000..9b0460e Binary files /dev/null and b/public/images/Status_Icons/exclamation.png differ diff --git a/public/images/Status_Icons/tick-circle.png b/public/images/Status_Icons/tick-circle.png new file mode 100755 index 0000000..210b1a6 Binary files /dev/null and b/public/images/Status_Icons/tick-circle.png differ diff --git a/public/images/Status_Icons/traffic-cone.png b/public/images/Status_Icons/traffic-cone.png new file mode 100755 index 0000000..394dba0 Binary files /dev/null and b/public/images/Status_Icons/traffic-cone.png differ diff --git a/public/images/favicon.ico b/public/images/favicon.ico new file mode 100755 index 0000000..210b1a6 Binary files /dev/null and b/public/images/favicon.ico differ diff --git a/public/less/bootstrap.less b/public/less/bootstrap.less new file mode 100644 index 0000000..e69de29 diff --git a/public/scripts/3rdparty/bootstrap-alerts.js b/public/scripts/3rdparty/bootstrap-alerts.js new file mode 100644 index 0000000..37bb430 --- /dev/null +++ b/public/scripts/3rdparty/bootstrap-alerts.js @@ -0,0 +1,113 @@ +/* ========================================================== + * bootstrap-alerts.js v1.4.0 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2011 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function( $ ){ + + "use strict" + + /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) + * ======================================================= */ + + var transitionEnd + + $(document).ready(function () { + + $.support.transition = (function () { + var thisBody = document.body || document.documentElement + , thisStyle = thisBody.style + , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined + return support + })() + + // set CSS transition event type + if ( $.support.transition ) { + transitionEnd = "TransitionEnd" + if ( $.browser.webkit ) { + transitionEnd = "webkitTransitionEnd" + } else if ( $.browser.mozilla ) { + transitionEnd = "transitionend" + } else if ( $.browser.opera ) { + transitionEnd = "oTransitionEnd" + } + } + + }) + + /* ALERT CLASS DEFINITION + * ====================== */ + + var Alert = function ( content, options ) { + this.settings = $.extend({}, $.fn.alert.defaults, options) + this.$element = $(content) + .delegate(this.settings.selector, 'click', this.close) + } + + Alert.prototype = { + + close: function (e) { + var $element = $(this).parent('.alert-message') + + e && e.preventDefault() + $element.removeClass('in') + + function removeElement () { + $element.remove() + } + + $.support.transition && $element.hasClass('fade') ? + $element.bind(transitionEnd, removeElement) : + removeElement() + } + + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + $.fn.alert = function ( options ) { + + if ( options === true ) { + return this.data('alert') + } + + return this.each(function () { + var $this = $(this) + + if ( typeof options == 'string' ) { + return $this.data('alert')[options]() + } + + $(this).data('alert', new Alert( this, options )) + + }) + } + + $.fn.alert.defaults = { + selector: '.close' + } + + $(document).ready(function () { + new Alert($('body'), { + selector: '.alert-message[data-alert] .close' + }) + }) + +}( window.jQuery || window.ender ); \ No newline at end of file diff --git a/public/scripts/3rdparty/bootstrap-dropdown.js b/public/scripts/3rdparty/bootstrap-dropdown.js new file mode 100644 index 0000000..cab0ec2 --- /dev/null +++ b/public/scripts/3rdparty/bootstrap-dropdown.js @@ -0,0 +1,55 @@ +/* ============================================================ + * bootstrap-dropdown.js v1.4.0 + * http://twitter.github.com/bootstrap/javascript.html#dropdown + * ============================================================ + * Copyright 2011 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function( $ ){ + + "use strict" + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function ( selector ) { + return this.each(function () { + $(this).delegate(selector || d, 'click', function (e) { + var li = $(this).parent('li') + , isActive = li.hasClass('open') + + clearMenus() + !isActive && li.toggleClass('open') + return false + }) + }) + } + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + var d = 'a.menu, .dropdown-toggle' + + function clearMenus() { + $(d).parent('li').removeClass('open') + } + + $(function () { + $('html').bind("click", clearMenus) + $('body').dropdown( '[data-dropdown] a.menu, [data-dropdown] .dropdown-toggle' ) + }) + +}( window.jQuery || window.ender ); \ No newline at end of file diff --git a/public/scripts/3rdparty/bootstrap-modal.js b/public/scripts/3rdparty/bootstrap-modal.js new file mode 100644 index 0000000..be2315a --- /dev/null +++ b/public/scripts/3rdparty/bootstrap-modal.js @@ -0,0 +1,260 @@ +/* ========================================================= + * bootstrap-modal.js v1.4.0 + * http://twitter.github.com/bootstrap/javascript.html#modal + * ========================================================= + * Copyright 2011 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function( $ ){ + + "use strict" + + /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) + * ======================================================= */ + + var transitionEnd + + $(document).ready(function () { + + $.support.transition = (function () { + var thisBody = document.body || document.documentElement + , thisStyle = thisBody.style + , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined + return support + })() + + // set CSS transition event type + if ( $.support.transition ) { + transitionEnd = "TransitionEnd" + if ( $.browser.webkit ) { + transitionEnd = "webkitTransitionEnd" + } else if ( $.browser.mozilla ) { + transitionEnd = "transitionend" + } else if ( $.browser.opera ) { + transitionEnd = "oTransitionEnd" + } + } + + }) + + + /* MODAL PUBLIC CLASS DEFINITION + * ============================= */ + + var Modal = function ( content, options ) { + this.settings = $.extend({}, $.fn.modal.defaults, options) + this.$element = $(content) + .delegate('.close', 'click.modal', $.proxy(this.hide, this)) + + if ( this.settings.show ) { + this.show() + } + + return this + } + + Modal.prototype = { + + toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + that.$element + .appendTo(document.body) + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + + return this + } + + , hide: function (e) { + e && e.preventDefault() + + if ( !this.isShown ) { + return this + } + + var that = this + this.isShown = false + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + + return this + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + // firefox drops transitionEnd events :{o + var that = this + , timeout = setTimeout(function () { + that.$element.unbind(transitionEnd) + hideModal.call(that) + }, 500) + + this.$element.one(transitionEnd, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal (that) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop ( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + if ( this.isShown && this.settings.backdrop ) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('
') + .appendTo(document.body) + + if ( this.settings.backdrop != 'static' ) { + this.$backdrop.click($.proxy(this.hide, this)) + } + + if ( doAnimate ) { + this.$backdrop[0].offsetWidth // force reflow + } + + this.$backdrop.addClass('in') + + doAnimate ? + this.$backdrop.one(transitionEnd, callback) : + callback() + + } else if ( !this.isShown && this.$backdrop ) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade')? + this.$backdrop.one(transitionEnd, $.proxy(removeBackdrop, this)) : + removeBackdrop.call(this) + + } else if ( callback ) { + callback() + } + } + + function removeBackdrop() { + this.$backdrop.remove() + this.$backdrop = null + } + + function escape() { + var that = this + if ( this.isShown && this.settings.keyboard ) { + $(document).bind('keyup.modal', function ( e ) { + if ( e.which == 27 ) { + that.hide() + } + }) + } else if ( !this.isShown ) { + $(document).unbind('keyup.modal') + } + } + + + /* MODAL PLUGIN DEFINITION + * ======================= */ + + $.fn.modal = function ( options ) { + var modal = this.data('modal') + + if (!modal) { + + if (typeof options == 'string') { + options = { + show: /show|toggle/.test(options) + } + } + + return this.each(function () { + $(this).data('modal', new Modal(this, options)) + }) + } + + if ( options === true ) { + return modal + } + + if ( typeof options == 'string' ) { + modal[options]() + } else if ( modal ) { + modal.toggle() + } + + return this + } + + $.fn.modal.Modal = Modal + + $.fn.modal.defaults = { + backdrop: false + , keyboard: false + , show: false + } + + + /* MODAL DATA- IMPLEMENTATION + * ========================== */ + + $(document).ready(function () { + $('body').delegate('[data-controls-modal]', 'click', function (e) { + e.preventDefault() + var $this = $(this).data('show', true) + $('#' + $this.attr('data-controls-modal')).modal( $this.data() ) + }) + }) + +}( window.jQuery || window.ender ); \ No newline at end of file diff --git a/public/scripts/3rdparty/bootstrap-popover.js b/public/scripts/3rdparty/bootstrap-popover.js new file mode 100644 index 0000000..c637784 --- /dev/null +++ b/public/scripts/3rdparty/bootstrap-popover.js @@ -0,0 +1,90 @@ +/* =========================================================== + * bootstrap-popover.js v1.4.0 + * http://twitter.github.com/bootstrap/javascript.html#popover + * =========================================================== + * Copyright 2011 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================================================== */ + + +!function( $ ) { + + "use strict" + + var Popover = function ( element, options ) { + this.$element = $(element) + this.options = options + this.enabled = true + this.fixTitle() + } + + /* NOTE: POPOVER EXTENDS BOOTSTRAP-TWIPSY.js + ========================================= */ + + Popover.prototype = $.extend({}, $.fn.twipsy.Twipsy.prototype, { + + setContent: function () { + var $tip = this.tip() + $tip.find('.title')[this.options.html ? 'html' : 'text'](this.getTitle()) + $tip.find('.content p')[this.options.html ? 'html' : 'text'](this.getContent()) + $tip[0].className = 'popover' + } + + , hasContent: function () { + return this.getTitle() || this.getContent() + } + + , getContent: function () { + var content + , $e = this.$element + , o = this.options + + if (typeof this.options.content == 'string') { + content = $e.attr(this.options.content) + } else if (typeof this.options.content == 'function') { + content = this.options.content.call(this.$element[0]) + } + + return content + } + + , tip: function() { + if (!this.$tip) { + this.$tip = $('') + .html(this.options.template) + } + return this.$tip + } + + }) + + + /* POPOVER PLUGIN DEFINITION + * ======================= */ + + $.fn.popover = function (options) { + if (typeof options == 'object') options = $.extend({}, $.fn.popover.defaults, options) + $.fn.twipsy.initWith.call(this, options, Popover, 'popover') + return this + } + + $.fn.popover.defaults = $.extend({} , $.fn.twipsy.defaults, { + placement: 'right' + , content: 'data-content' + , template: '{0}{current}{2}'+b+" ",a.extract&&(h+="on line "+a.line+", column "+(a.column+1)+":
"+e.replace(/\[(-?\d)\]/g,function(b,c){return parseInt(a.line)+parseInt(c)||""}).replace(/\{(\d)\}/g,function(b,c){return a.extract[parseInt(c)]||""}).replace(/\{current\}/,a.extract[1].slice(0,a.column)+''+a.extract[1].slice(a.column)+"")),f.innerHTML=h,p([".less-error-message ul, .less-error-message li {","list-style-type: none;","margin-right: 15px;","padding: 4px 0;","margin: 0;","}",".less-error-message label {","font-size: 12px;","margin-right: 15px;","padding: 4px 0;","color: #cc7777;","}",".less-error-message pre {","color: #ee4444;","padding: 4px 0;","margin: 0;","display: inline-block;","}",".less-error-message pre.ctx {","color: #dd4444;","}",".less-error-message h3 {","font-size: 20px;","font-weight: bold;","padding: 15px 0 5px 0;","margin: 0;","}",".less-error-message a {","color: #10a","}",".less-error-message .error {","color: red;","font-weight: bold;","padding-bottom: 2px;","border-bottom: 1px dashed red;","}"].join("\n"),{title:"error-message"}),f.style.cssText=["font-family: Arial, sans-serif","border: 1px solid #e00","background-color: #eee","border-radius: 5px","-webkit-border-radius: 5px","-moz-border-radius: 5px","color: #e00","padding: 15px","margin-bottom: 15px"].join(";"),d.env=="development"&&(g=setInterval(function(){document.body&&(document.getElementById(c)?document.body.replaceChild(f,document.getElementById(c)):document.body.insertBefore(f,document.body.firstChild),clearInterval(g))},10))}Array.isArray||(Array.isArray=function(a){return Object.prototype.toString.call(a)==="[object Array]"||a instanceof Array}),Array.prototype.forEach||(Array.prototype.forEach=function(a,b){var c=this.length>>>0;for(var d=0;d
+ * {foreach item=$debugItem from=$debugData}
+ * // Switch on $debugItem.type
+ * {switch $debugItem.type}
+ * {case 1}
+ * {case "invalid_field"}
+ * // Case checks for string and numbers.
+ * {/case}
+ * {case $postError}
+ * {case $getError|cat:"_ajax"|lower}
+ * // Case checks can also use variables and modifiers.
+ * {break}
+ * {default}
+ * // Default case is supported.
+ * {/switch}
+ * {/foreach}
+ *
+ *
+ * Note in the above example that the break statements work exactly as expected. Also the switch and default
+ * tags can take the break attribute. If set they will break automatically before the next case is printed.
+ *
+ * Both blocks produce the same switch logic:
+ *
+ * {case 1 break}
+ * Code 1
+ * {case 2}
+ * Code 2
+ * {default break}
+ * Code 3
+ *
+ *
+ *
+ * {case 1}
+ * Code 1
+ * {break}
+ * {case 2}
+ * Code 2
+ * {default}
+ * Code 3
+ * {break}
+ *
+ *
+ * Finally, there is an alternate long hand style for the switch statments that you may need to use in some cases.
+ *
+ *
+ * {switch var=$type}
+ * {case value="box" break}
+ * {case value="line"}
+ * {break}
+ * {default}
+ * {/switch}
+ *
+ */
+
+//Register the post and pre filters as they are not auto-registered.
+$this->registerFilter('post', 'smarty_postfilter_switch');
+
+class Smarty_Compiler_Switch extends Smarty_Internal_CompileBase {
+ public $required_attributes = array('var');
+ public $optional_attributes = array();
+ public $shorttag_order = array('var');
+
+/**
+ * Start a new switch statement.
+ * A variable must be passed to switch on.
+ * Also, the switch can only directly contain {case} and {default} tags.
+ *
+ * @param string $tag_arg
+ * @param Smarty_Compiler $smarty
+ * @return string
+ */
+ public function compile($args, $compiler){
+ $this->compiler = $compiler;
+ $attr = $this->_get_attributes($args);
+ $_output = '';
+
+ $this->_open_tag('switch',array($compiler->tag_nocache));
+
+ if (is_array($attr['var'])) {
+ $_output .= "tpl_vars[".$attr['var']['var']."])) \$_smarty_tpl->tpl_vars[".$attr['var']['var']."] = new Smarty_Variable;";
+ $_output .= "switch (\$_smarty_tpl->tpl_vars[".$attr['var']['var']."]->value = ".$attr['var']['value']."){?>";
+ } else {
+ $_output .= '';
+ }
+ return $_output;
+ }
+}
+
+class Smarty_Compiler_Case extends Smarty_Internal_CompileBase {
+ public $required_attributes = array('value');
+ public $optional_attributes = array('break');
+ public $shorttag_order = array('value', 'break');
+
+/**
+ * Print out a case line for this switch.
+ * A condition must be passed to match on.
+ * This can only go in {switch} tags.
+ * If break is passed, a {break} will be rendered before the next case.
+ *
+ * @param string $tag_arg
+ * @param Smarty_Compiler $smarty
+ * @return string
+ */
+ public function compile($args, $compiler){
+ $this->compiler = $compiler;
+ $attr = $this->_get_attributes($args);
+ $_output = '';
+
+ list($last_tag, $last_attr) = $this->compiler->_tag_stack[count($this->compiler->_tag_stack) - 1];
+
+ if($last_tag == 'case')
+ {
+ list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
+ if($last_attr[0])
+ $_output .= '';
+ }
+ $this->_open_tag('case', array(isset($attr['break']) ? $attr['break'] : false, $compiler->tag_nocache));
+
+ if (is_array($attr['value'])) {
+ $_output .= "tpl_vars[".$attr['value']['var']."])) \$_smarty_tpl->tpl_vars[".$attr['value']['var']."] = new Smarty_Variable;";
+ $_output .= "case \$_smarty_tpl->tpl_vars[".$attr['value']['var']."]->value = ".$attr['value']['value'].":?>";
+ } else {
+ $_output .= '';
+ }
+ return $_output;
+ }
+}
+
+class Smarty_Compiler_Default extends Smarty_Internal_CompileBase {
+ public $required_attributes = array();
+ public $optional_attributes = array('break');
+ public $shorttag_order = array('break');
+
+/**
+ * Print out a default line for this switch.
+ * This can only go in {switch} tags.
+ * If break is passed, a {break} will be rendered before the next case.
+ *
+ * @param string $tag_arg
+ * @param Smarty_Compiler $smarty
+ * @return string
+ */
+ public function compile($args, $compiler){
+ $this->compiler = $compiler;
+ $attr = $this->_get_attributes($args);
+ $_output = '';
+
+ list($last_tag, $last_attr) = $this->compiler->_tag_stack[count($this->compiler->_tag_stack) - 1];
+ if($last_tag == 'case')
+ {
+ list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
+ if($last_attr[0])
+ $_output .= '';
+ }
+ $this->_open_tag('case', array(isset($attr['break']) ? $attr['break'] : false, $compiler->tag_nocache));
+
+ $_output .= '';
+
+ return $_output;
+ }
+}
+
+
+class Smarty_Compiler_Break extends Smarty_Internal_CompileBase {
+ public $required_attributes = array();
+ public $optional_attributes = array();
+ public $shorttag_order = array();
+
+/**
+ * Print out a break command for the switch.
+ * This can only go inside of {case} tags.
+ *
+ * @param string $tag_arg
+ * @param Smarty_Compiler $smarty
+ * @return string
+ */
+
+ public function compile($args, $compiler){
+ $this->compiler = $compiler;
+ $attr = $this->_get_attributes($args);
+
+ list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
+
+ return '';
+ }
+}
+
+class Smarty_Compiler_Caseclose extends Smarty_Internal_CompileBase {
+ public $required_attributes = array();
+ public $optional_attributes = array();
+ public $shorttag_order = array();
+
+/**
+ * Print out a break command for the switch.
+ * This can only go inside of {case} tags.
+ *
+ * @param string $tag_arg
+ * @param Smarty_Compiler $smarty
+ * @return string
+ */
+
+ public function compile($args, $compiler){
+ $this->compiler = $compiler;
+ $attr = $this->_get_attributes($args);
+
+ list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
+
+ return '';
+ }
+}
+
+class Smarty_Compiler_Switchclose extends Smarty_Internal_CompileBase {
+ public $required_attributes = array();
+ public $optional_attributes = array();
+ public $shorttag_order = array();
+
+/**
+ * End a switch statement.
+ *
+ * @param string $tag_arg
+ * @param Smarty_Compiler $smarty
+ * @return string
+ */
+
+ public function compile($args, $compiler){
+ $this->compiler = $compiler;
+ $attr = $this->_get_attributes($args);
+
+ list($last_tag, $last_attr) = $this->compiler->_tag_stack[count($this->compiler->_tag_stack) - 1];
+ if(($last_tag == 'case' || $last_tag == 'default'))
+ list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
+ list($compiler->tag_nocache) = $this->_close_tag(array('switch'));
+
+ return '';
+ }
+}
+
+/**
+ * Filter the template after it is generated to fix switch bugs.
+ * Remove any spaces after the 'switch () {' code and before the first case. Any tabs or spaces
+ * for layout would cause php errors witch this reged will fix.
+ *
+ * @param string $compiled
+ * @param Smarty_Compiler $smarty
+ * @return string
+ */
+function smarty_postfilter_switch($compiled, &$smarty) {
+ // Remove the extra spaces after the start of the switch tag and before the first case statement.
+ return preg_replace('/({ ?\?>)\s+(<\?php case)/', "$1\n$2", $compiled);
+}
+?>
\ No newline at end of file
diff --git a/source/webui/pages/admin.php b/source/webui/pages/admin.php
new file mode 100644
index 0000000..00ff05a
--- /dev/null
+++ b/source/webui/pages/admin.php
@@ -0,0 +1,137 @@
+auth();
+$config = $main->config();
+$request = $main->request();
+$session = $main->session();
+
+if ( ! $auth->isAuthenticated() || ! $auth->hasPermission(StatusBoard_Permission::PERM_Administrator)) {
+ throw new StatusBoard_Exception_NotAuthorised();
+}
+
+$activity = null;
+$success = true;
+
+$destination = $request->get('tab', 'summary');
+
+if ($request->exists('do')) {
+ $activity = $request->get('do');
+ switch ($activity) {
+
+ case 'add-service': {
+ $name = StatusBoard_Main::issetelse($_POST['name'], 'Sihnon_Exception_InvalidParameters');
+ $description = StatusBoard_Main::issetelse($_POST['description'], 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ $service = StatusBoard_Service::newService($name, $description);
+
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The service was created succesfully.',
+ );
+ } catch (StatusBoard_Exception_InvalidContent $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The service was not added due to invalid parameters being passed.',
+ );
+ }
+
+ } break;
+
+ case 'delete-service': {
+ $service_id = $request->get('id', 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ $service = StatusBoard_Service::fromId($service_id);
+ $service->delete();
+
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The Service was deleted successfully.',
+ );
+ } catch (Sihnon_Exception_ResultCountMismatch $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The Service was not deleted as the object requested could not be found.',
+ );
+ }
+
+ } break;
+
+ case 'save-settings': {
+ $supported_settings = array(
+ 'site_title' => 'site.title',
+ 'debug_display_exceptions' => 'debug.display_exceptions',
+ 'cache_base_dir' => 'cache.base_dir',
+ 'templates_tmp_path' => 'templates.tmp_path',
+ );
+
+ $dirty = false;
+ foreach ($supported_settings as $param => $setting) {
+ $value = StatusBoard_Main::issetelse($_POST[$param]);
+ if ($value && $value != $config->get($setting)) {
+ $config->set($setting, $value);
+ $dirty = true;
+ }
+ }
+
+ if ($dirty) {
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'Settings were saved successfully.',
+ );
+ } else {
+ $messages[] = array(
+ 'severity' => 'warning',
+ 'content' => 'Settings were not saved as no changes were necessary.',
+ );
+ }
+
+ } break;
+
+ default: {
+ $messages[] = array(
+ 'severity' => 'warning',
+ 'content' => "The activity '{$activity}' is not supported.",
+ );
+ }
+ }
+
+ $destination = "admin/tab/{$destination}/";
+
+ $session->set('messages', $messages);
+ StatusBoard_Page::redirect($destination);
+}
+
+$this->smarty->assign('tab', $destination);
+if ($destination == 'summary') {
+ $this->smarty->assign('service_count', StatusBoard_Service::count());
+ $this->smarty->assign('site_count', StatusBoard_Site::count());
+ $this->smarty->assign('incident_counts', StatusBoard_Incident::counts());
+
+ $incidents_near_deadline = StatusBoard_Incident::allNearDeadline();
+ usort($incidents_near_deadline, array('StatusBoard_Incident', 'compareEstimatedEndTimes'));
+
+ $incidents_past_deadline = StatusBoard_Incident::allPastDeadline();
+ usort($incidents_past_deadline, array('StatusBoard_Incident', 'compareEstimatedEndTimes'));
+
+ $this->smarty->assign('incidents_near_deadline', $incidents_near_deadline);
+ $this->smarty->assign('incidents_past_deadline', $incidents_past_deadline);
+}
+
+
+$services = StatusBoard_Service::all();
+$this->smarty->assign('services', $services);
+
+$users = $auth->listUsers();
+$this->smarty->assign('users', $users);
+
+// Quick Settings
+$this->smarty->assign('debug_displayexceptions', $config->get('debug.display_exceptions'));
+$this->smarty->assign('cache_basedir', $config->get('cache.base_dir'));
+$this->smarty->assign('templates_tmppath', $config->get('templates.tmp_path'));
+$this->smarty->assign('site_title', $config->get('site.title'));
+$this->smarty->assign('messages', $messages);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/admin/add-incident.php b/source/webui/pages/admin/add-incident.php
new file mode 100644
index 0000000..2465ba0
--- /dev/null
+++ b/source/webui/pages/admin/add-incident.php
@@ -0,0 +1,95 @@
+request();
+$auth = $main->auth();
+$session = $main->session();
+
+if ( ! $auth->isAuthenticated() || ! $auth->hasPermission(StatusBoard_Permission::PERM_UpdateIncidents)) {
+ throw new StatusBoard_Exception_NotAuthorised();
+}
+
+$messages = array();
+
+if ($request->exists('do')) {
+
+ $service_id = StatusBoard_Main::issetelse($_POST['service'], 'Sihnon_Exception_InvalidParameters');
+ $site_id = StatusBoard_Main::issetelse($_POST['site'], 'Sihnon_Exception_InvalidParameters');
+ $reference = StatusBoard_Main::issetelse($_POST['reference'], 'Sihnon_Exception_InvalidParameters');
+ $description = StatusBoard_Main::issetelse($_POST['description'], 'Sihnon_Exception_InvalidParameters');
+ $status = StatusBoard_Main::issetelse($_POST['status'], 'Sihnon_Exception_InvalidParameters');
+ $start_time = StatusBoard_Main::issetelse($_POST['starttime'], 'Sihnon_Exception_InvalidParameters');
+ $estimated_end_time = StatusBoard_Main::issetelse($_POST['estimatedendtime'], 'Sihnon_Exception_InvalidParameters');
+
+ $incident = null;
+
+ try {
+ StatusBoard_Validation_Text::content(array($service_id, $site_id), StatusBoard_Validation_Text::Digit);
+ StatusBoard_Validation_Text::length($reference, 1, 32);
+ StatusBoard_Validation_Enum::validate($status, 'StatusBoard_Status', 'STATUS_');
+
+ $service = StatusBoard_Service::fromId($service_id);
+ $site = StatusBoard_Site::fromId($site_id);
+
+ $start_time = strtotime($start_time);
+ if ($start_time === null) {
+ throw new StatusBoard_Exception_InvalidParameters('starttime');
+ }
+ $estimated_end_time = strtotime($estimated_end_time);
+ if ($estimated_end_time === null) {
+ throw new StatusBoard_Exception_InvalidParameters('estimatedendtime');
+ }
+
+ $incident = $site->newIncident($reference, $description, $status, $start_time, $estimated_end_time);
+
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The incident was created succesfully.',
+ );
+ } catch (StatusBoard_Exception_ResultCountMismatch $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The incident was not created because the Service or Site could not be found.',
+ );
+ }
+
+ $session->set('messages', $messages);
+ StatusBoard_Page::redirect("admin/incident/service/{$service->id}/site/{$site->id}/id/{$incident->id}/");
+}
+
+$service_id = $request->get('service');
+$site_id = $request->get('site');
+
+$service = null;
+$site = null;
+
+$services = StatusBoard_Service::all();
+try {
+ if ($service_id) {
+ $service = StatusBoard_Service::fromId($service_id);
+ }
+ if ($site_id) {
+ $site = StatusBoard_Site::fromId($site_id);
+ }
+} catch (Sihnon_Exception_ResultCountMismatch $e) {
+ throw new StatusBoard_Exception_FileNotFound();
+}
+
+$all_sites = array();
+if ($service) {
+ $all_sites[$service->id] = $service->sites();
+} else {
+ foreach ($services as $all_service) {
+ $all_sites[$all_service->id] = $all_service->sites();
+ }
+}
+
+
+
+$this->smarty->assign('services', $services);
+$this->smarty->assign('service', $service);
+$this->smarty->assign('all_sites', $all_sites);
+$this->smarty->assign('site', $site);
+$this->smarty->assign('messages', $messages);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/admin/incident.php b/source/webui/pages/admin/incident.php
new file mode 100644
index 0000000..d84c651
--- /dev/null
+++ b/source/webui/pages/admin/incident.php
@@ -0,0 +1,113 @@
+request();
+$auth = $main->auth();
+$session = $main->session();
+
+if ( ! $auth->isAuthenticated() || ! $auth->hasPermission(StatusBoard_Permission::PERM_UpdateIncidents)) {
+ throw new StatusBoard_Exception_NotAuthorised();
+}
+
+$messages = array();
+
+$service_id = $request->get('service', 'Sihnon_Exception_InvalidParameters');
+$site_id = $request->get('site', 'Sihnon_Exception_InvalidParameters');
+$incident_id = $request->get('id', 'Sihnon_Exception_InvalidParameters');
+
+$service = null;
+$site = null;
+$incident = null;
+
+try {
+ $service = StatusBoard_Service::fromId($service_id);
+ $site = StatusBoard_Site::fromId($site_id);
+ $incident = StatusBoard_Incident::fromId($incident_id);
+} catch (Sihnon_Exception_ResultCountMismatch $e) {
+ throw new StatusBoard_Exception_FileNotFound();
+}
+
+if ($request->exists('do')) {
+ $activity = $request->get('do');
+ switch ($activity) {
+
+ case 'edit': {
+ $reference = StatusBoard_Main::issetelse($_POST['reference'], 'Sihnon_Exception_InvalidParameters');
+ $description = StatusBoard_Main::issetelse($_POST['description'], 'Sihnon_Exception_InvalidParameters');
+ $estimated_end_time = StatusBoard_Main::issetelse($_POST['estimatedendtime'], 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ StatusBoard_Validation_Text::length($reference, 1, 32);
+
+ $estimated_end_time = strtotime($estimated_end_time);
+ if ($estimated_end_time) {
+ $incident->reference = $reference;
+ $incident->description = $description;
+ $incident->estimated_end_time = $estimated_end_time;
+ $incident->save();
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The incident was updated succesfully.',
+ );
+ } else {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The incident was not modified because the value entered for estimated end time was not understood.',
+ );
+ }
+ } catch (StatusBoard_Exception_InvalidContent $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The incident was not modified due to invalid parameters being passed.',
+ );
+ }
+
+ } break;
+
+ case 'change-status': {
+ $status = StatusBoard_Main::issetelse($_POST['status'], 'Sihnon_Exception_InvalidParameters');
+ $description = StatusBoard_Main::issetelse($_POST['description'], 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ StatusBoard_Validation_Enum::validate($status, 'StatusBoard_Status', 'STATUS_');
+
+ $incident->changeStatus($status, $description);
+
+ if ($status == StatusBoard_Status::STATUS_Resolved) {
+ $incident->actual_end_time = time();
+ $incident->save();
+ }
+
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The incident status was changed successfully.',
+ );
+ } catch (StatusBoard_Exception_InvalidContent $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The status was not modified due to invalid parameters being passed.',
+ );
+ }
+ } break;
+
+ default: {
+ $messages[] = array(
+ 'severity' => 'warning',
+ 'content' => "The activity '{$activity}' is not supported.",
+ );
+ }
+ }
+
+ $session->set('messages', $messages);
+ StatusBoard_Page::redirect("admin/incident/service/{$service->id}/site/{$site->id}/id/{$incident->id}/");
+}
+
+$statuses = $incident->statusChanges();
+
+$this->smarty->assign('service', $service);
+$this->smarty->assign('site', $site);
+$this->smarty->assign('incident', $incident);
+$this->smarty->assign('statuses', $statuses);
+$this->smarty->assign('messages', $messages);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/admin/service.php b/source/webui/pages/admin/service.php
new file mode 100644
index 0000000..fa4bdd1
--- /dev/null
+++ b/source/webui/pages/admin/service.php
@@ -0,0 +1,110 @@
+request();
+$auth = $main->auth();
+$session = $main->session();
+
+if ( ! $auth->isAuthenticated() || ! $auth->hasPermission(StatusBoard_Permission::PERM_UpdateStatusBoards)) {
+ throw new StatusBoard_Exception_NotAuthorised();
+}
+
+$activity = null;
+$messages = array();
+
+$service_id = $request->get('id', 'Sihnon_Exception_InvalidParameters');
+$service = null;
+try {
+ $service = StatusBoard_Service::fromId($service_id);
+} catch (Sihnon_Exception_ResultCountMismatch $e) {
+ throw new StatusBoard_Exception_FileNotFound();
+}
+
+if ($request->exists('do')) {
+ $activity = $request->get('do');
+ switch ($activity) {
+
+ case 'edit': {
+ $name = StatusBoard_Main::issetelse($_POST['name'], 'Sihnon_Exception_InvalidParameters');
+ $description = StatusBoard_Main::issetelse($_POST['description'], 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ StatusBoard_Validation_Text::length($name, 1, 255);
+
+ $service->name = $name;
+ $service->description = $description;
+ $service->save();
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The service was updated succesfully.',
+ );
+ } catch (StatusBoard_Exception_InvalidContent $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The service was not modified due to invalid parameters being passed.',
+ );
+ }
+ } break;
+
+ case 'add-site': {
+ $name = StatusBoard_Main::issetelse($_POST['name'], 'Sihnon_Exception_InvalidParameters');
+ $description = StatusBoard_Main::issetelse($_POST['description'], 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ StatusBoard_Validation_Text::length($name, 1, 255);
+
+ $site = $service->newSite($name, $description);
+
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The site was created succesfully.',
+ );
+ } catch (StatusBoard_Exception_InvalidContent $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The site was not added due to invalid parameters being passed.',
+ );
+ }
+
+ } break;
+
+ case 'delete-site': {
+ $site_id = $request->get('site', 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ $site = StatusBoard_Site::fromId($site_id);
+ $site->delete();
+
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The Site was deleted successfully.',
+ );
+ } catch (Sihnon_Exception_ResultCountMismatch $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The Site was not deleted as the object requested could not be found.',
+ );
+ }
+
+ } break;
+
+ default: {
+ $messages[] = array(
+ 'severity' => 'warning',
+ 'content' => "The activity '{$activity}' is not supported.",
+ );
+ }
+ }
+
+ $session->set('messages', $messages);
+ StatusBoard_Page::redirect("admin/service/id/{$service->id}/");
+}
+
+
+$sites = $service->sites();
+
+$this->smarty->assign('service', $service);
+$this->smarty->assign('sites', $sites);
+$this->smarty->assign('messages', $messages);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/admin/site.php b/source/webui/pages/admin/site.php
new file mode 100644
index 0000000..4b95bfb
--- /dev/null
+++ b/source/webui/pages/admin/site.php
@@ -0,0 +1,76 @@
+request();
+$auth = $main->auth();
+$session = $main->session();
+
+if ( ! $auth->isAuthenticated() || ! $auth->hasPermission(StatusBoard_Permission::PERM_UpdateStatusBoards)) {
+ throw new StatusBoard_Exception_NotAuthorised();
+}
+
+$messages = array();
+
+$service_id = $request->get('service', 'Sihnon_Exception_InvalidParameters');
+$site_id = $request->get('id', 'Sihnon_Exception_InvalidParameters');
+
+$service = null;
+$site = null;
+
+try {
+ $service = StatusBoard_Service::fromId($service_id);
+ $site = StatusBoard_Site::fromId($site_id);
+} catch (Sihnon_Exception_ResultCountMismatch $e) {
+ throw new StatusBoard_Exception_FileNotFound();
+}
+
+if ($request->exists('do')) {
+ $activity = $request->get('do');
+ switch ($activity) {
+
+ case 'edit': {
+ $name = StatusBoard_Main::issetelse($_POST['name'], 'Sihnon_Exception_InvalidParameters');
+ $description = StatusBoard_Main::issetelse($_POST['description'], 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ StatusBoard_Validation_Text::length($name, 1, 255);
+
+ $site->name = $name;
+ $site->description = $description;
+ $site->save();
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The site was updated succesfully.',
+ );
+ } catch (StatusBoard_Exception_InvalidContent $e) {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The site was not modified due to invalid parameters being passed.',
+ );
+ }
+
+ } break;
+
+ default: {
+ $messages[] = array(
+ 'severity' => 'warning',
+ 'content' => "The activity '{$activity}' is not supported.",
+ );
+ }
+
+ }
+
+ $session->set('messages', $messages);
+ StatusBoard_Page::redirect("admin/site/service/{$service->id}/id/{$site->id}/");
+}
+
+
+
+$open_incidents = $site->openIncidents();
+
+$this->smarty->assign('service', $service);
+$this->smarty->assign('site', $site);
+$this->smarty->assign('open_incidents', $open_incidents);
+$this->smarty->assign('messages', $messages);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/errors/401.php b/source/webui/pages/errors/401.php
new file mode 100644
index 0000000..779d46a
--- /dev/null
+++ b/source/webui/pages/errors/401.php
@@ -0,0 +1,9 @@
+request();
+
+$this->smarty->assign('requested_page', $req->request_string());
+
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/errors/404.php b/source/webui/pages/errors/404.php
new file mode 100644
index 0000000..779d46a
--- /dev/null
+++ b/source/webui/pages/errors/404.php
@@ -0,0 +1,9 @@
+request();
+
+$this->smarty->assign('requested_page', $req->request_string());
+
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/errors/unhandled-exception.php b/source/webui/pages/errors/unhandled-exception.php
new file mode 100644
index 0000000..95bbc07
--- /dev/null
+++ b/source/webui/pages/errors/unhandled-exception.php
@@ -0,0 +1,10 @@
+config();
+
+$this->smarty->assign('display_exceptions', $config->get('debug.display_exceptions'));
+$this->smarty->assign('exception', $exception);
+$this->smarty->assign('exception_type', get_class($exception));
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/home.php b/source/webui/pages/home.php
new file mode 100644
index 0000000..2a2616d
--- /dev/null
+++ b/source/webui/pages/home.php
@@ -0,0 +1,16 @@
+config();
+$auth = $main->auth();
+
+
+$services = StatusBoard_Service::all();
+$this->smarty->assign('services', $services);
+
+$this->smarty->assign('site_title', $config->get('site.title', 'Status Board'));
+
+$display_admin_links = ($auth->isAuthenticated() && $auth->isAdministrator());
+$this->smarty->assign('display_admin_links', $display_admin_links);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/incident.php b/source/webui/pages/incident.php
new file mode 100644
index 0000000..657d368
--- /dev/null
+++ b/source/webui/pages/incident.php
@@ -0,0 +1,39 @@
+
+request();
+$auth = $main->auth();
+
+$incident_id = $request->get('id', 'Sihnon_Exception_InvalidParameters');
+
+try
+ {
+ $incident = StatusBoard_Incident::fromId($incident_id);
+ $site_id = $incident->site;
+ $site = StatusBoard_Site::fromId($site_id);
+ $service_id = $site->service;
+ $service = StatusBoard_Service::fromId($service_id);
+ }
+catch (Sihnon_Exception_ResultCountMismatch $e)
+ {
+ throw new StatusBoard_Exception_FileNotFound();
+
+ }
+
+
+
+$statuses = $incident->statusChanges();
+
+
+$this->smarty->assign('service', $service);
+$this->smarty->assign('site', $site);
+$this->smarty->assign('incident', $incident);
+$this->smarty->assign('statuses', $statuses);
+$this->smarty->assign('messages', $messages);
+
+$display_admin_links = ($auth->isAuthenticated() && $auth->isAdministrator());
+$this->smarty->assign('display_admin_links', $display_admin_links);
+
+
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/login.php b/source/webui/pages/login.php
new file mode 100644
index 0000000..f8a4da8
--- /dev/null
+++ b/source/webui/pages/login.php
@@ -0,0 +1,31 @@
+request();
+$auth = $main->auth();
+$log = $main->log();
+
+$authenticated = false;
+$authentication_failed = false;
+
+if ($request->exists('do')) {
+ $username = StatusBoard_Main::issetelse($_POST['username'], 'Sihnon_Exception_InvalidParameters');
+ $password = StatusBoard_Main::issetelse($_POST['password'], 'Sihnon_Exception_InvalidParameters');
+
+ try {
+ $auth->authenticate($username, $password);
+ $authenticated = true;
+
+ StatusBoard_Page::redirect('home');
+
+ } catch (Sihnon_Exception_UnknownUser $e) {
+ $authentication_failed = true;
+ } catch (Sihnon_Exception_IncorrectPassword $e) {
+ $authentication_failed = true;
+ }
+}
+
+$this->smarty->assign('authentication', $authenticated);
+$this->smarty->assign('authentication_failed', $authentication_failed);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/logout.php b/source/webui/pages/logout.php
new file mode 100644
index 0000000..0e8fa0d
--- /dev/null
+++ b/source/webui/pages/logout.php
@@ -0,0 +1,8 @@
+auth();
+$auth->deauthenticate();
+
+StatusBoard_Page::redirect('home');
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/navigation.php b/source/webui/pages/navigation.php
new file mode 100644
index 0000000..a2c3dc4
--- /dev/null
+++ b/source/webui/pages/navigation.php
@@ -0,0 +1,15 @@
+auth();
+if ($auth->isAuthenticated()) {
+ $authenticated = true;
+ $user = $auth->authenticatedUser();
+}
+
+$this->smarty->assign('authenticated', $authenticated);
+$this->smarty->assign('auth', $auth);
+$this->smarty->assign('user', $user);
+?>
\ No newline at end of file
diff --git a/source/webui/pages/status.php b/source/webui/pages/status.php
new file mode 100644
index 0000000..ba44d78
--- /dev/null
+++ b/source/webui/pages/status.php
@@ -0,0 +1,32 @@
+request();
+$auth = $main->auth();
+
+$service_id = $request->get('service', 'Sihnon_Exception_InvalidParameters');
+$site_id = $request->get('id', 'Sihnon_Exception_InvalidParameters');
+
+$start = $request->get('start');
+$end = $request->get('end');
+
+$service = null;
+$site = null;
+
+try {
+ $service = StatusBoard_Service::fromId($service_id);
+ $site = StatusBoard_Site::fromId($site_id);
+} catch (Sihnon_Exception_ResultCountMismatch $e) {
+ throw new StatusBoard_Exception_FileNotFound();
+}
+
+$services = StatusBoard_Service::all();
+
+$this->smarty->assign('service', $service);
+$this->smarty->assign('site', $site);
+$this->smarty->assign('start', $start);
+$this->smarty->assign('end', $end);
+
+$display_admin_links = ($auth->isAuthenticated() && $auth->isAdministrator());
+$this->smarty->assign('display_admin_links', $display_admin_links);
+
+?>
\ No newline at end of file
diff --git a/source/webui/pages/usercp.php b/source/webui/pages/usercp.php
new file mode 100644
index 0000000..a1fc75a
--- /dev/null
+++ b/source/webui/pages/usercp.php
@@ -0,0 +1,55 @@
+request();
+$auth = $main->auth();
+$session = $main->session();
+
+$activity = null;
+
+if ($request->exists('do')) {
+ $activity = $request->get('do');
+ switch ($activity) {
+
+ case 'change-password': {
+ $current_password = StatusBoard_Main::issetelse($_POST['currentpassword'], 'Sihnon_Exception_InvalidParameters');
+ $new_password = StatusBoard_Main::issetelse($_POST['newpassword'], 'Sihnon_Exception_InvalidParameters');
+ $confirm_password = StatusBoard_Main::issetelse($_POST['confirmpassword'], 'Sihnon_Exception_InvalidParameters');
+
+ $user = $auth->authenticatedUser();
+ if ($user->checkPassword($current_password)) {
+ if ($new_password == $confirm_password) {
+ $auth->changePassword($new_password);
+ $messages[] = array(
+ 'severity' => 'success',
+ 'content' => 'The password has been changed successfully.',
+ );
+ } else {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The passwords did not match.',
+ );
+ }
+ } else {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => 'The current password was incorrect.',
+ );
+ }
+ } break;
+
+ default: {
+ $messages[] = array(
+ 'severity' => 'error',
+ 'content' => "The activity '{$activity}' was not recognised.",
+ );
+ } break;
+ }
+
+ $session->set('messages', $messages);
+ StatusBoard_Page::redirect("usercp/");
+}
+
+$this->smarty->assign('activity', $activity);
+$this->smarty->assign('messages', $messages);
+?>
\ No newline at end of file
diff --git a/source/webui/templates/admin.tpl b/source/webui/templates/admin.tpl
index 30404ce..09d7030 100644
--- a/source/webui/templates/admin.tpl
+++ b/source/webui/templates/admin.tpl
@@ -1 +1,232 @@
-TODO
\ No newline at end of file
+
+
++ There {StatusBoard_Formatting::pluralise(count($incidents_near_deadline), 'is', 'are')} {$incidents_near_deadline|count} {StatusBoard_Formatting::pluralise(count($incidents_near_deadline), 'incident', 'incidents')} + within 1 hour of the current estimated end time. +
+ {if $incidents_near_deadline} ++ There {StatusBoard_Formatting::pluralise(count($incidents_near_deadline), 'is', 'are')} {$incidents_past_deadline|count} {StatusBoard_Formatting::pluralise(count($incidents_past_deadline), 'incident', 'incidents')} + already past the set estimated end time. +
+ {if $incidents_past_deadline} +| Statistic | +Count | +
|---|---|
| Services | +{$service_count} | +
| Sites | +{$site_count} | +
| Incidents | +{array_sum(array_values($incident_counts))} | +
| Incident Statistics | +Count | +Major | +{$incident_counts[StatusBoard_Status::STATUS_Major]} | + +
| Significant | +{$incident_counts[StatusBoard_Status::STATUS_Significant]} | +
| Minor | +{$incident_counts[StatusBoard_Status::STATUS_Minor]} | +
| Planned Maintenance | +{$incident_counts[StatusBoard_Status::STATUS_Maintenance]} | +
| Resolved | +{$incident_counts[StatusBoard_Status::STATUS_Resolved]} | +
Click on a Service to edit its properties, or access any of the sites defined under it.
+| Service | +Description | +Action | + + + {foreach from=$services item=service} +
|---|---|---|
| + {$service->name|escape:html} + | ++ {$service->description|escape:html} + | ++ + + | +
Use this form to define a new service
+Use this form to add a new incident
+Use this form to update the existing Service
+Use this form to update the current status of an incident
+The table display an audit log of changes to this incident
+| Date/Time | +Status | +Description | + + + {foreach from=$statuses item=status} +
|---|---|---|
|
+ {StatusBoard_DateTime::fuzzyTime($status->ctime)} + {$status->ctime|date_format:'y-m-d H:i:s'} + |
+ {StatusBoard_Status::name($status->status)} | +{$status->description|escape:html} | +
Use this form to update the existing Service
+Currently the following sites that are defined for the service {$service->name|escape:html}, Edit the site or delete it from the service here, to add a new one use the form below
+| Site | +Description | +Action | + + + {foreach from=$sites item=site} +
|---|---|---|
| + {$site->name|escape:html} + | ++ {$site->description|escape:html} + | ++ + + | +
Use this form to define a new site to the service {$service->name|escape:html}
+Use this form to update the existing Service
+| Reference | +Description | +Status | +Action | + + + {foreach from=$open_incidents item=incident} +
|---|---|---|---|
| + {$incident->reference|escape:html} + | ++ {$site->description|escape:html} + | ++ {StatusBoard_Status::name($incident->currentStatus())} + | ++ + + | +
There are no open incidents for this site . If you need to open one, use the form below.
+ {/if} ++ Click the button to open the Add Incident page. +
++ The page you requested ({$requested_page|escape:html}) could not be opened. + Please ensure you are logged in and have permission to access this page. +
\ No newline at end of file diff --git a/source/webui/templates/errors/404.tpl b/source/webui/templates/errors/404.tpl index 607707a..956dab8 100644 --- a/source/webui/templates/errors/404.tpl +++ b/source/webui/templates/errors/404.tpl @@ -1,6 +1,6 @@- The file you requested ({$requested_page}) could not be found. + The file you requested ({$requested_page|escape:html}) could not be found. If you typed in the address manually, check that you have spelled it correctly, or if you followed a link, let us know and we'll look into it.
\ No newline at end of file diff --git a/source/webui/templates/errors/unhandled-exception.tpl b/source/webui/templates/errors/unhandled-exception.tpl index 58ce7f6..65309f5 100644 --- a/source/webui/templates/errors/unhandled-exception.tpl +++ b/source/webui/templates/errors/unhandled-exception.tpl @@ -7,40 +7,56 @@An unhandled exception was caught during the page template processing. The full details are shown below:
-| Exception | -{$exception_type} | -
|---|---|
| File | -{$exception->getFile()} | -
| Line | -{$exception->getLine()} | -
| Message | -{$exception->getMessage()} | -
| Stack Trace | -{$exception->getTrace()|print_r} |
-
Note: Exception details should not be displayed on production systems.
- Disable the debug.show_exceptions
+ Disable the Display Exceptions
setting to omit the exception details from this page.
| Service / Site | +Now | + {foreach from=array(0,1,2,3,4,5,6) item=day} + {if $day == 0} +Today | + {else} +{mktime(0,0,0,date("n"),date("j")-$day)|date_format:"M j"} | + {/if} + {/foreach} +|||||
|---|---|---|---|---|---|---|---|---|
| + {if $display_admin_links} + {$service->name} + {else} + {$service->name} + {/if} + | +||||||||
| + {if $display_admin_links} + {$site->name|escape:html} + {else} + {$site->name} + {/if} + | ++ {$status=$site->status()} + {include file="fragments/site-status.tpl" nocache date=null start=null end=null} + | + {foreach from=array(0,1,2,3,4,5,6) item=day} + {$start=mktime(0,0,0,date("n"),date("j")-$day)} + {$end=mktime(0,0,0,date("n"),date("j")-$day+1)} + {$date=mktime(0,0,0,date("n"),date("j")-$day)|date_format:"jM"} + {$incidentsDuring=$site->openIncidentsDuring($start, $end)} + {$statusDuring=StatusBoard_Incident::highestSeverityStatusBetween($incidentsDuring, $start, $end)} ++ {include file="fragments/site-status.tpl" nocache start=$start end=$end status=$statusDuring incidents=$incidentsDuring} + | + {/foreach} +||||||
This page details the history of incident: {$incident->reference|escape:html}
+Service: {$service->name|escape:html}
+Site: {$site->name|escape:html}
+Opened: {$incident->start_time|date_format:'H:i d-M-y'}
+Estimated End: {ucwords(StatusBoard_DateTime::fuzzyTime($incident->estimated_end_time))}
+ +The table display an audit log of changes to this incident
+| Date/Time | +Status | +Description | + + + {foreach from=$statuses item=status} +
|---|---|---|
|
+ {ucwords(StatusBoard_DateTime::fuzzyTime($status->ctime))} + {$status->ctime|date_format:'H:i d-M-y'} + |
+ {StatusBoard_Status::name($status->status)} | +{$status->description|escape:html} | +