OMG-4586 - Initial production dashboard push

This commit is contained in:
Faisal Zia 2018-11-05 15:38:59 +05:00
commit 1473e39784
62 changed files with 3045 additions and 0 deletions

0
.dancer Normal file
View file

24
MANIFEST Normal file
View file

@ -0,0 +1,24 @@
MANIFEST
MANIFEST.SKIP
.dancer
Makefile.PL
cpanfile
config.yml
bin/app.psgi
environments/production.yml
environments/development.yml
lib/ProdDashboard.pm
public/favicon.ico
public/404.html
public/500.html
public/dispatch.fcgi
public/dispatch.cgi
public/css/error.css
public/css/style.css
public/images/perldancer-bg.jpg
public/images/perldancer.jpg
public/javascripts/jquery.js
t/002_index_route.t
t/001_base.t
views/index.tt
views/layouts/main.tt

17
MANIFEST.SKIP Normal file
View file

@ -0,0 +1,17 @@
^\.git\/
maint
^tags$
.last_cover_stats
Makefile$
^blib
^pm_to_blib
^.*.bak
^.*.old
^t.*sessions
^cover_db
^.*\.log
^.*\.swp$
MYMETA.*
^.gitignore
^.svn\/
^ProdDashboard-

26
Makefile.PL Normal file
View file

@ -0,0 +1,26 @@
use strict;
use warnings;
use ExtUtils::MakeMaker;
# Normalize version strings like 6.30_02 to 6.3002,
# so that we can do numerical comparisons on it.
my $eumm_version = $ExtUtils::MakeMaker::VERSION;
$eumm_version =~ s/_//;
WriteMakefile(
NAME => 'ProdDashboard',
AUTHOR => q{YOUR NAME <youremail@example.com>},
VERSION_FROM => 'lib/ProdDashboard.pm',
ABSTRACT => 'YOUR APPLICATION ABSTRACT',
($eumm_version >= 6.3001
? ('LICENSE'=> 'perl')
: ()),
PL_FILES => {},
PREREQ_PM => {
'Test::More' => 0,
'YAML' => 0,
'Dancer2' => 0.206000,
},
dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
clean => { FILES => 'ProdDashboard-*' },
);

10
bin/app.psgi Normal file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use ProdDashboard;
ProdDashboard->to_app;

69
config.yml Normal file
View file

@ -0,0 +1,69 @@
# This is the main configuration file of your Dancer2 app
# env-related settings should go to environments/$env.yml
# all the settings in this file will be loaded at Dancer's startup.
# Your application's name
appname: "ProdDashboard"
# The default layout to use for your application (located in
# views/layouts/main.tt)
layout: "main"
# when the charset is set to UTF-8 Dancer2 will handle for you
# all the magic of encoding and decoding. You should not care
# about unicode within your app when this setting is set (recommended).
charset: "UTF-8"
# template engine
# simple: default and very basic template engine
# template_toolkit: TT
template: "template_toolkit"
# YAML stores on disk
session: YAML
plugins:
'Ajax':
content_type: 'application/json'
engines:
template:
template_toolkit:
start_tag: '<%'
end_tag: '%>'
encoding: 'utf8'
session:
YAML:
session_dir: /tmp/dancer-sessions
# template: "template_toolkit"
# engines:
# template:
# template_toolkit:
# start_tag: '<%'
# end_tag: '%>'
# session engine
#
# Simple: in-memory session store - Dancer2::Session::Simple
# YAML: session stored in YAML files - Dancer2::Session::YAML
#
# Check out metacpan for other session storage options:
# https://metacpan.org/search?q=Dancer2%3A%3ASession&search_type=modules
#
# Default value for 'cookie_name' is 'dancer.session'. If you run multiple
# Dancer apps on the same host then you will need to make sure 'cookie_name'
# is different for each app.
#
#engines:
# session:
# Simple:
# cookie_name: testapp.session
#
#engines:
# session:
# YAML:
# cookie_name: eshop.session
# is_secure: 1
# is_http_only: 1

11
cpanfile Normal file
View file

@ -0,0 +1,11 @@
requires "Dancer2" => "0.206000";
recommends "YAML" => "0";
recommends "URL::Encode::XS" => "0";
recommends "CGI::Deurl::XS" => "0";
recommends "HTTP::Parser::XS" => "0";
on "test" => sub {
requires "Test::More" => "0";
requires "HTTP::Request::Common" => "0";
};

1
dbscripts/auto/backup.sh Normal file
View file

@ -0,0 +1 @@
PGPASSWORD=oliver99 pg_dump proddashboard -Fc -U postgres -h localhost -f prod_dashboard_temp.sql

329
dbscripts/auto/db_deploy.pl Normal file
View file

@ -0,0 +1,329 @@
#!/usr/bin/perl
###############################################################################
#
# DB deployment automation
# must always be run from the target auto folder.
# it will expect the sprint folders to be at ../s1 etc
#
# always backup DB before a deploy
# pg_dump OMG -Fc -U postgres -h localhost > omg_backup.sql
###############################################################################
use DBI;
use Data::Dumper;
use strict;
my $C_PGEXEC = 'PGPASSWORD=oliver99 psql -U postgres -d proddashboard -h localhost -1 -f ';
my $C_LOG = "pg_log.txt";
my $C_TEAM1_LOW = 1;
my $C_TEAM1_HIGH = 99;
my $C_TEAM2_LOW = 100;
my $C_TEAM2_HIGH = 199;
my $C_SPRINT_DONE = 1;
my $C_TEAM_DONE = 2;
###############################################################################
# writeLog
###############################################################################
sub writeLog {
my ($status, $cmd, $output) = @_;
open(LOGFILE,">>$C_LOG") or die("Can't open log file.\n");
print LOGFILE ("$status RUNNING $cmd\n");
print LOGFILE ("$output\n");
print LOGFILE ("--------------------------------------------------------------------------------------\n");
close(LOGFILE);
}
###############################################################################
# checkInit
# check if deploy init had been done
###############################################################################
sub checkInit{
my ($dbh) = @_;
my $drecs = 0;
#check whether deploy table exists
my $sql = qq{SELECT count(*) FROM information_schema.tables where table_name = 'deploy' and table_schema = 'public'};
my $sth = $dbh->prepare($sql);
$sth->execute();
($drecs) = $sth->fetchrow_array;
print "Found $drecs instances of deploy\n";
return $drecs;
}
###############################################################################
# updateDeploy
# add completed deployment item to tracking db
###############################################################################
sub updateDeploy{
my ($dbh, $sprint, $item, $file) = @_;
my $cmd = $C_PGEXEC.$file;
my $log = `$cmd`;
if ( $? == -1 )
{
writeLog("FAILED", $cmd, $log);
print "command $cmd failed: $!\n";
exit(0);
}
else{
writeLog("OK", $cmd, $log);
print $log.'\n';
eval{
my $sql = qq{insert into public.deploy (sprint_id, script_id, status) values (?, ?, ?)};
my $sth = $dbh->prepare($sql);
$sth->execute( $sprint, $item, 't' );
};
if ($@){
print "Error updating Deploy entry for sprint $sprint, item $item \n";
exit(0);
}
}
}
###############################################################################
# checkFailuresExist
# check if failed item found
###############################################################################
sub checkFailuresExist{
my ($dbh) = @_;
my $failed;
# check last item of last sprint
my $sql = qq{SELECT count(*) FROM public.deploy where status != TRUE};
my $sth = $dbh->prepare($sql);
$sth->execute();
($failed) = $sth->fetchrow_array;
return $failed;
}
###############################################################################
# checkLastDeploy
# check the last sprint/id deployed for this team range
###############################################################################
sub checkLastDeploy{
my ($dbh, $high, $low) = @_;
my %state;
my @row;
# check last item of last sprint
my $sql = qq{SELECT sprint_id as sprintid, script_id as scriptid FROM public.deploy WHERE script_id <= ? and script_id >= ? order by sprint_id DESC, script_id DESC limit 1};
my $sth = $dbh->prepare($sql);
$sth->execute( $high, $low );
@row = $sth->fetchrow_array;
if (@row){
print "values found\n";
$state{sprint} = $row[0];
$state{item} = $row[1];
}
else{
print "values NOT found\n";
$state{sprint} = 1;
$state{item} = $low - 1;
}
#print Dumper( \@row );
return \%state;
}
###############################################################################
# deploy
# deploy all incomplete items from specified item + 1
# it will only deploy items in this team range
###############################################################################
sub deploy{
my ($dbh, $sprint, $item, $low) = @_;
print "Beginning deployment at sprint $sprint item $item \n";
my $cont = 1;
do {
# check for next sprint item to deploy
$item++;
my $spath = "../s".$sprint."/deploy/".$item."_*.sql";
print "checking $spath \n";
my @files = glob($spath);
my $numfiles = @files;
if ( $numfiles ){
print "found deploy file for sprint $sprint item $item as $files[0] \n";
updateDeploy( $dbh, $sprint, $item, $files[0] );
}
else{
print "no more items in current sprint\n";
# check next sprint
$sprint++;
$item = $low-1;
$spath = "../s".$sprint."/deploy/";
if (-e $spath){
print "checking next sprint $sprint\n";
}
else{
print "Deploy complete\n";
$cont = 0;
}
}
} while ($cont);
}
###############################################################################
# deploy_next
# deploy all incomplete items from specified item + 1 within this
# sprint only.
# it will only deploy items in this team range
###############################################################################
sub deploy_next{
my ($dbh, $sprint, $item, $low) = @_;
print "Beginning deployment at sprint $sprint item $item low $low \n";
my $cont = 1;
do {
# check for next sprint item to deploy
$item++;
my $spath = "../s".$sprint."/deploy/".$item."_*.sql";
print "checking $spath \n";
my @files = glob($spath);
my $numfiles = @files;
if ( $numfiles ){
print "found deploy file for sprint $sprint item $item as $files[0] \n";
updateDeploy( $dbh, $sprint, $item, $files[0] );
}
else{
print "no more items in current sprint\n";
# check next sprint
$sprint++;
$item = $low-1;
$spath = "../s".$sprint."/deploy/";
if (-e $spath){
print "next sprint available\n";
return $C_SPRINT_DONE;
}
else{
print "Deploy complete\n";
return $C_TEAM_DONE;
}
}
} while ($cont);
}
sub main{
print "Begin Deploy\n";
my $sync = 0;
my $dbh = DBI->connect('dbi:Pg:dbname=proddashboard;host=localhost','pgdev','pgdev',{AutoCommit=>1,RaiseError=>1,PrintError=>0});
# check if init done
my $init = checkInit( $dbh );
if ($init){
print "init done\n";
}
else{
print "init has not been run - please run init_deploy.sh from the command line\n";
exit(0);
}
# now get the last deployment entry on this system
#my ($sprint, $item) = checkLastDeploy( $dbh );
if ( checkFailuresExist( $dbh ) ){
print "Failures exist - please resolve first\n";
exit(0);
}
print "no failures found - checking last deploy point for TEAM1\n";
# get current deploy state for both teams
my ($team1state, $team2state, $first, $second, $cont, $sprint, $res1, $res2);
$team1state = checkLastDeploy( $dbh, $C_TEAM1_HIGH, $C_TEAM1_LOW );
$team2state = checkLastDeploy( $dbh, $C_TEAM2_HIGH, $C_TEAM2_LOW );
$team2state->{low} = $C_TEAM2_LOW;
$team2state->{high} = $C_TEAM2_HIGH;
$team1state->{low} = $C_TEAM1_LOW;
$team1state->{high} = $C_TEAM1_HIGH;
# choose lowest sprint
if ($team2state->{sprint} < $team1state->{sprint}){
$first = $team2state;
$second = $team1state;
}
else{
$first = $team1state;
$second = $team2state;
}
# now start on first team and sync sprint by sprint
$cont = 1;
$sprint = $first->{sprint};
do{
# check if sprints are in sync and make sure team1 is first
if ($second->{sprint} == $first->{sprint}){
$first = $team1state;
$second = $team2state;
$sync = 1;
}
# run
$res1 = deploy_next( $dbh, $first->{sprint}, $first->{item}, $first->{low} );
# move to next sprint
$first->{sprint} = $first->{sprint} + 1;
$first->{item} = $first->{low} - 1;
# check if second team sprint caught up
$res2 = 0;
if ($sync){
# run this sprint
$res2 = deploy_next( $dbh, $second->{sprint}, $second->{item}, $second->{low} );
$second->{sprint} = $second->{sprint} + 1;
$second->{item} = $second->{low} - 1;
}
# now check if this completed the deploy or more sprints left
if ($res1 == $C_TEAM_DONE){
$cont = 0;
}
} while ($cont);
print "End Deploy\n";
}
#
# run main
#
main();
# end

View file

@ -0,0 +1,23 @@
/************************************************************
* Author : Geetha Nair<Geethanair@oliver-marketing.com
* Date : 28/April/2015
* Description : Create a table called deploy that holds details of db scripts run on master
*************************************************************/
-- Table: "deploy"
-- DROP TABLE "deploy";
CREATE TABLE "deploy"
(
"id" serial NOT NULL,
"sprint_id" integer,
"script_id" integer,
"status" boolean,
CONSTRAINT "deploy_pkey" PRIMARY KEY ("id")
)
WITH (
OIDS=FALSE
);

View file

@ -0,0 +1 @@
PGPASSWORD=oliver99 psql -U postgres -d proddashboard -h localhost -1 -f deploy_table_create.sql

View file

@ -0,0 +1 @@
PGPASSWORD=oliver99 pg_restore -U postgres -h localhost -n public --dbname=proddashboard -c -1 prod_dashboard_temp.sql

113
dbscripts/auto/ss_deploy.pl Normal file
View file

@ -0,0 +1,113 @@
#!/usr/bin/perl
###############################################################################
#
# DB deployment automation(support scripts)
# must always be run from the target auto folder.
#
# always backup DB before a deploy
# pg_dump dashboard -Fc -U postgres -h localhost > dashboard_backup.sql
###############################################################################
use DBI;
use Data::Dumper;
use strict;
my $C_PGEXEC = 'PGPASSWORD=oliver99 psql -U postgres -d proddashboard -h localhost -1 -f ';
my $C_LOG = "pg_log.txt";
###############################################################################
# writeLog
###############################################################################
sub writeLog {
my ($status, $cmd, $output) = @_;
open(LOGFILE,">>$C_LOG") or die("Can't open log file.\n");
print LOGFILE ("$status RUNNING $cmd\n");
print LOGFILE ("$output\n");
print LOGFILE ("--------------------------------------------------------------------------------------\n");
close(LOGFILE);
}
###############################################################################
# updateDeploy
# add completed deployment item to tracking db
###############################################################################
sub updateDeploy{
my ($dbh, $file) = @_;
my $cmd = $C_PGEXEC.$file;
my $log = `$cmd`;
if ( $? == -1 )
{
writeLog("FAILED", $cmd, $log);
print "command $cmd failed: $!\n";
exit(0);
}
else{
writeLog("OK", $cmd, $log);
print $log.'\n';
}
}
###############################################################################
# deploy_next
# deploy all incomplete items from specified item + 1 within this
# sprint only.
###############################################################################
sub deploy_next{
my ($dbh, $item) = @_;
print "Beginning deployment at item $item \n";
my $spath = "../supportscripts/".$item."_*.sql";
print "checking $spath \n";
my @files = glob($spath);
my $numfiles = @files;
if ( $numfiles ){
print "found deploy file for item $item as $files[0] \n";
updateDeploy( $dbh, $files[0] );
}
else{
print "no more items\n";
return -1;
}
}
sub main{
print "Begin Support Deploy\n";
my $dbh = DBI->connect('dbi:Pg:dbname=proddashboard;host=localhost','pgdev','pgdev',{AutoCommit=>1,RaiseError=>1,PrintError=>0});
my $cont = 1;
my $item = 1;
my $res1 = 0;
do{
# run
$res1 = deploy_next( $dbh, $item );
# move to next item
$item++;
# now check if this completed the deploy or more sprints left
if ($res1 == -1){
$cont = 0;
}
} while ($cont);
print "End Support Deploy\n";
}
#
# run main
#
main();
# end

View file

@ -0,0 +1,24 @@
/********************************************************************************************
* Author : Faisal Zia<faisal.zia@jintech.com
* Date : 02/11/2018
* Description : Create production servers table
* Jira Ticket : https://oliveruk.atlassian.net/browse/OMG-4586
*********************************************************************************************/
-- Table: public.prod_servers
DROP TABLE IF EXISTS public.prod_servers;
CREATE TABLE public.prod_servers
(
id serial,
server_id character varying(255),
status integer, -- defines Servers RAG status: 1:Green, 2:Amber, 3:Red
last_active timestamp with time zone,
CONSTRAINT prod_servers_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
);
COMMENT ON COLUMN public.prod_servers.status IS 'defines Servers RAG status: 1:Green, 2:Amber, 3:Red';

View file

@ -0,0 +1,44 @@
/********************************************************************************************
* Author : Faisal Zia<faisal.zia@jintech.com
* Date : 02/11/2018
* Description : Register production servers
* Jira Ticket : https://oliveruk.atlassian.net/browse/OMG-4586
*********************************************************************************************/
-- Function: register_prod_servers(character varying, integer, integer)
DROP FUNCTION IF EXISTS register_prod_servers(character varying, integer, integer);
CREATE OR REPLACE FUNCTION register_prod_servers(
server_num character varying,
server_status integer)
RETURNS integer AS
$BODY$
DECLARE
lastid int;
BEGIN
-- insert a new record into prod_servers
WITH upsert AS (UPDATE "public"."prod_servers"
SET
status = server_status,
last_active =
CASE WHEN server_status = 1
THEN now()
ELSE last_active
END
WHERE server_id = server_num
RETURNING server_id)
INSERT INTO "public"."prod_servers" ( server_id, last_active, status )
SELECT server_num, now(), server_status
WHERE NOT EXISTS (SELECT 1 FROM upsert WHERE server_id = server_num)
RETURNING server_id INTO lastid;
IF lastid IS NULL THEN
lastid := server_num;
END IF;
RETURN lastid;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;

View file

@ -0,0 +1,30 @@
/********************************************************************************************
* Author : Faisal Zia<faisal.zia@jintech.com
* Date : 02/11/2018
* Description : Get list of all production servers
* Jira Ticket : https://oliveruk.atlassian.net/browse/OMG-4586
*********************************************************************************************/
-- Function: get_all_prod_servers()
DROP FUNCTION IF EXISTS get_all_prod_servers();
CREATE OR REPLACE FUNCTION get_all_prod_servers()
RETURNS TABLE(
id integer,
server_number character varying,
last_active double precision,
status integer
) AS
$BODY$
BEGIN
RETURN QUERY
SELECT
prodser.id,
prodser.server_id,
floor(EXTRACT(EPOCH FROM prodser.last_active)),
prodser.status
FROM "public"."prod_servers" AS prodser;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;

View file

@ -0,0 +1,24 @@
/*********************************************************************************************************************************
* Author : Faisal Zia <faisal.zia@jintech.com>
* Date : 05/11/2018
* Description : create prod_logs table
* Jira Ticket : https://oliveruk.atlassian.net/browse/OMG-4586
***********************************************************************************************************************************/
-- Table: prod_logs
DROP TABLE IF EXISTS prod_logs;
CREATE TABLE prod_logs
(
id serial,
log_time timestamp with time zone,
activity character varying(80),
prod_server_id character varying,
message text,
CONSTRAINT prod_logs_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
);

View file

@ -0,0 +1,30 @@
/********************************************************************************************
* Author : Faisal Zia<faisal.zia@jintech.com>
* Date : 05/11/2018
* Description : Create a log entry in otp log table
* Jira Ticket : https://oliveruk.atlassian.net/browse/OMG-4586
*********************************************************************************************/
-- Function: create_prod_log_entry(character varying, character varying, text)
DROP FUNCTION IF EXISTS create_prod_log_entry(character varying, character varying, text);
CREATE OR REPLACE FUNCTION create_prod_log_entry(
prodserver_id character varying,
log_activity character varying,
log_msg text)
RETURNS integer AS
$BODY$
DECLARE
lastid int;
BEGIN
-- insert a new record into prod_logs
INSERT INTO "public"."prod_logs" (prod_server_id, activity, message, log_time)
VALUES (prodserver_id, log_activity, log_msg, now()) RETURNING id INTO lastid;
RETURN lastid;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;

View file

@ -0,0 +1,44 @@
# configuration file for development environment
# the logger engine to use
# console: log messages to STDOUT (your console where you started the
# application server)
# file: log message to a file in log/
logger: "console"
# the log level for this environment
# core is the lowest, it shows Dancer2's core log messages as well as yours
# (debug, info, warning and error)
log: "core"
# should Dancer2 consider warnings as critical errors?
warnings: 1
# should Dancer2 show a stacktrace when an 5xx error is caught?
# if set to yes, public/500.html will be ignored and either
# views/500.tt, 'error_template' template, or a default error template will be used.
show_errors: 1
# print the banner
startup_info: 1
# database
plugins:
Database:
# default DB connection
driver: 'Pg'
database: 'proddashboard'
host: '127.0.0.1'
port: 5432
username: 'pgdev'
password: 'pgdev'
connection_check_threshold: 20
dbi_params:
RaiseError: 1
AutoCommit: 1
log_queries: 1
domain: 'http://192.168.154.138:5000/'
admin_email: "devops@oliver.agency"
sender_email: "devops@oliver.agency"

View file

@ -0,0 +1,16 @@
# configuration file for production environment
# only log warning and error messsages
log: "warning"
# log message to a file in logs/
logger: "file"
# don't consider warnings critical
warnings: 0
# hide errors
show_errors: 0
# disable server tokens in production environments
no_server_tokens: 1

19
lib/ProdDashboard.pm Normal file
View file

@ -0,0 +1,19 @@
package ProdDashboard;
use Dancer2;
use Dancer2::Plugin::Ajax;
use homepage::homepage_controller;
use production::production_controller;
our $VERSION = '0.1';
prefix "/";
#################################################################
# Route Home page
#################################################################
get '/' => \&homepage_controller::showHomepage;
########## API ROUTES ###########################################
post '/api/registerServer' => \&production_controller::registerServers;
true;

BIN
lib/email/.DS_Store vendored Normal file

Binary file not shown.

36
lib/email/email_helper.pm Normal file
View file

@ -0,0 +1,36 @@
package email_helper;
use Dancer2 appname => 'ProdDashboard';
use utils::Mailer;
use Data::Dumper;
use production::production_logs_helper;
# sends email
sub sendMail{
my ($fromAddress, $toemailAddresses, $emailSubject, $emailBody) = @_;
if( $fromAddress eq "" ){
production_logs_helper::addLogEntry(0, 'MAIL ISSUE', 'Issue in sending status email. From address missing.');
return 0;
}
if( $toemailAddresses eq "" ){
production_logs_helper::addLogEntry(0, 'MAIL ISSUE', 'Issue in sending status email. To address missing.');
return 0;
}
my $mailSuccessflag = Mailer::send_mail(
$fromAddress,
$toemailAddresses,
$emailBody,
$emailSubject
);
if(! $mailSuccessflag ){
production_logs_helper::addLogEntry(0, 'MAIL ISSUE', 'Issue in sending status email.');
return 0;
}else{
return 1;
}
}
1;

View file

@ -0,0 +1,7 @@
package errors::AccessDenied;
use Moo;
extends 'errors::Error';
1;

View file

@ -0,0 +1,7 @@
package errors::ActivityDenied;
use Moo;
extends 'errors::Error';
1;

View file

@ -0,0 +1,7 @@
package errors::ArgumentError;
use Moo;
extends 'errors::Error';
1;

24
lib/errors/Error.pm Normal file
View file

@ -0,0 +1,24 @@
package errors::Error;
use strict;
use warnings;
use Moo;
with 'Throwable';
has 'message' => (
is => 'ro',
required => 1,
);
has 'target' => (
is => 'ro'
);
use overload
'""' => \&to_string;
sub to_string { $_[0]->message; }
1;

View file

@ -0,0 +1,12 @@
package errors::customExceptions;
use errors::AccessDenied;
use errors::ActivityDenied;
use errors::ArgumentError;
use errors::dbError;
use errors::invalidInput;
use errors::invalidObject;
use errors::lostResource;
use errors::notFound;
1;

249
lib/errors/dbError.pm Normal file
View file

@ -0,0 +1,249 @@
package errors::dbError;
use Dancer2 appname => 'ProdDashboard';
use Moo;
extends 'errors::Error';
my %ERROR_STATES = (
"03000" => "SqlStatementNotYetComplete",
"08000" => "ConnectionException",
"08003" => "ConnectionDoesNotExist",
"08006" => "ConnectionFailure",
"08001" => "SqlclientUnableToEstablishSqlconnection",
"08004" => "SqlserverRejectedEstablishmentOfSqlconnection",
"08007" => "TransactionResolutionUnknown",
"08P01" => "ProtocolViolation",
"09000" => "TriggeredActionException",
"0A000" => "FeatureNotSupported",
"0B000" => "InvalidTransactionInitiation",
"0F000" => "LocatorException",
"0F001" => "InvalidLocatorSpecification",
"0L000" => "InvalidGrantor",
"0LP01" => "InvalidGrantOperation",
"0P000" => "InvalidRoleSpecification",
"20000" => "CaseNotFound",
"21000" => "CardinalityViolation",
"22000" => "DataException",
"2202E" => "ArraySubscriptError",
"22021" => "CharacterNotInRepertoire",
"22008" => "DatetimeFieldOverflow",
"22012" => "DivisionByZero",
"22005" => "ErrorInAssignment",
"2200B" => "EscapeCharacterConflict",
"22022" => "IndicatorOverflow",
"22015" => "IntervalFieldOverflow",
"2201E" => "InvalidArgumentForLogarithm",
"22014" => "InvalidArgumentForNtileFunction",
"22016" => "InvalidArgumentForNthValueFunction",
"2201F" => "InvalidArgumentForPowerFunction",
"2201G" => "InvalidArgumentForWidthBucketFunction",
"22018" => "InvalidCharacterValueForCast",
"22007" => "InvalidDatetimeFormat",
"22019" => "InvalidEscapeCharacter",
"2200D" => "InvalidEscapeOctet",
"22025" => "InvalidEscapeSequence",
"22P06" => "NonstandardUseOfEscapeCharacter",
"22010" => "InvalidIndicatorParameterValue",
"22023" => "InvalidParameterValue",
"2201B" => "InvalidRegularExpression",
"2201W" => "InvalidRowCountInLimitClause",
"2201X" => "InvalidRowCountInResultOffsetClause",
"22009" => "InvalidTimeZoneDisplacementValue",
"2200C" => "InvalidUseOfEscapeCharacter",
"2200G" => "MostSpecificTypeMismatch",
"22004" => "NullValueNotAllowed",
"22002" => "NullValueNoIndicatorParameter",
"22003" => "NumericValueOutOfRange",
"22026" => "StringDataLengthMismatch",
"22001" => "StringDataRightTruncation",
"22011" => "SubstringError",
"22027" => "TrimError",
"22024" => "UnterminatedCString",
"2200F" => "ZeroLengthCharacterString",
"22P01" => "FloatingPointException",
"22P02" => "InvalidTextRepresentation",
"22P03" => "InvalidBinaryRepresentation",
"22P04" => "BadCopyFileFormat",
"22P05" => "UntranslatableCharacter",
"2200L" => "NotAnXmlDocument",
"2200M" => "InvalidXmlDocument",
"2200N" => "InvalidXmlContent",
"2200S" => "InvalidXmlComment",
"2200T" => "InvalidXmlProcessingInstruction",
"23000" => "IntegrityConstraintViolation",
"23001" => "RestrictViolation",
"23502" => "NotNullViolation",
"23503" => "ForeignKeyViolation",
"23505" => "UniqueViolation",
"23514" => "CheckViolation",
"23P01" => "ExclusionViolation",
"24000" => "InvalidCursorState",
"25000" => "InvalidTransactionState",
"25001" => "ActiveSqlTransaction",
"25002" => "BranchTransactionAlreadyActive",
"25008" => "HeldCursorRequiresSameIsolationLevel",
"25003" => "InappropriateAccessModeForBranchTransaction",
"25004" => "InappropriateIsolationLevelForBranchTransaction",
"25005" => "NoActiveSqlTransactionForBranchTransaction",
"25006" => "ReadOnlySqlTransaction",
"25007" => "SchemaAndDataStatementMixingNotSupported",
"25P01" => "NoActiveSqlTransaction",
"25P02" => "InFailedSqlTransaction",
"26000" => "InvalidSqlStatementName",
"27000" => "TriggeredDataChangeViolation",
"28000" => "InvalidAuthorizationSpecification",
"28P01" => "InvalidPassword",
"2B000" => "DependentPrivilegeDescriptorsStillExist",
"2BP01" => "DependentObjectsStillExist",
"2D000" => "InvalidTransactionTermination",
"2F000" => "SqlRoutineException",
"34000" => "InvalidCursorName",
"38000" => "ExternalRoutineException",
"39000" => "ExternalRoutineInvocationException",
"3B000" => "SavepointException",
"3B001" => "InvalidSavepointSpecification",
"3D000" => "InvalidCatalogName",
"3F000" => "InvalidSchemaName",
"40000" => "TransactionRollback",
"40002" => "TransactionIntegrityConstraintViolation",
"40001" => "SerializationFailure",
"40003" => "StatementCompletionUnknown",
"40P01" => "DeadlockDetected",
"42000" => "SyntaxErrorOrAccessRuleViolation",
"42601" => "SyntaxError",
"42501" => "InsufficientPrivilege",
"42846" => "CannotCoerce",
"42803" => "GroupingError",
"42P20" => "WindowingError",
"42P19" => "InvalidRecursion",
"42830" => "InvalidForeignKey",
"42602" => "InvalidName",
"42622" => "NameTooLong",
"42939" => "ReservedName",
"42804" => "DatatypeMismatch",
"42P18" => "IndeterminateDatatype",
"42P21" => "CollationMismatch",
"42P22" => "IndeterminateCollation",
"42809" => "WrongObjectType",
"42703" => "UndefinedColumn",
"42883" => "UndefinedFunction",
"42P01" => "UndefinedTable",
"42P02" => "UndefinedParameter",
"42704" => "UndefinedObject",
"42701" => "DuplicateColumn",
"42P03" => "DuplicateCursor",
"42P04" => "DuplicateDatabase",
"42723" => "DuplicateFunction",
"42P05" => "DuplicatePreparedStatement",
"42P06" => "DuplicateSchema",
"42P07" => "DuplicateTable",
"42712" => "DuplicateAlias",
"42710" => "DuplicateObject",
"42702" => "AmbiguousColumn",
"42725" => "AmbiguousFunction",
"42P08" => "AmbiguousParameter",
"42P09" => "AmbiguousAlias",
"42P10" => "InvalidColumnReference",
"42611" => "InvalidColumnDefinition",
"42P11" => "InvalidCursorDefinition",
"42P12" => "InvalidDatabaseDefinition",
"42P13" => "InvalidFunctionDefinition",
"42P14" => "InvalidPreparedStatementDefinition",
"42P15" => "InvalidSchemaDefinition",
"42P16" => "InvalidTableDefinition",
"42P17" => "InvalidObjectDefinition",
"44000" => "WithCheckOptionViolation",
"53000" => "InsufficientResources",
"53100" => "DiskFull",
"53200" => "OutOfMemory",
"53300" => "TooManyConnections",
"54000" => "ProgramLimitExceeded",
"54001" => "StatementTooComplex",
"54011" => "TooManyColumns",
"54023" => "TooManyArguments",
"55000" => "ObjectNotInPrerequisiteState",
"55006" => "ObjectInUse",
"55P02" => "CantChangeRuntimeParam",
"55P03" => "LockNotAvailable",
"57000" => "OperatorIntervention",
"57014" => "QueryCanceled",
"57P01" => "AdminShutdown",
"57P02" => "CrashShutdown",
"57P03" => "CannotConnectNow",
"57P04" => "DatabaseDropped",
"58000" => "SystemError", # added manually
"58030" => "IoError",
"58P01" => "UndefinedFile",
"58P02" => "DuplicateFile",
"F0000" => "ConfigFileError",
"F0001" => "LockFileExists",
"HV000" => "FdwError",
"HV005" => "FdwColumnNameNotFound",
"HV002" => "FdwDynamicParameterValueNeeded",
"HV010" => "FdwFunctionSequenceError",
"HV021" => "FdwInconsistentDescriptorInformation",
"HV024" => "FdwInvalidAttributeValue",
"HV007" => "FdwInvalidColumnName",
"HV008" => "FdwInvalidColumnNumber",
"HV004" => "FdwInvalidDataType",
"HV006" => "FdwInvalidDataTypeDescriptors",
"HV091" => "FdwInvalidDescriptorFieldIdentifier",
"HV00B" => "FdwInvalidHandle",
"HV00C" => "FdwInvalidOptionIndex",
"HV00D" => "FdwInvalidOptionName",
"HV090" => "FdwInvalidStringLengthOrBufferLength",
"HV00A" => "FdwInvalidStringFormat",
"HV009" => "FdwInvalidUseOfNullPointer",
"HV014" => "FdwTooManyHandles",
"HV001" => "FdwOutOfMemory",
"HV00P" => "FdwNoSchemas",
"HV00J" => "FdwOptionNameNotFound",
"HV00K" => "FdwReplyHandle",
"HV00Q" => "FdwSchemaNotFound",
"HV00R" => "FdwTableNotFound",
"HV00L" => "FdwUnableToCreateExecution",
"HV00M" => "FdwUnableToCreateReply",
"HV00N" => "FdwUnableToEstablishConnection",
"P0000" => "PlpgsqlError",
"P0001" => "RaiseException",
"P0002" => "NoDataFound",
"P0003" => "TooManyRows",
"S1000" => "GeneralError", # added manually
"XX000" => "InternalError",
"XX001" => "DataCorrupted",
"XX002" => "IndexCorrupted",
);
has 'state' => (
is => 'ro',
required => 1,
);
has 'code' => (
is => 'ro',
required => 1,
);
sub type {
my $self = shift;
return $ERROR_STATES{$self->state};
}
sub from_handle {
my ($class, $handle, $caller) = @_;
my $error_state = $handle->state;
my $error_msg = $handle->errstr;
my $error_code = $handle->err;
error "$caller query failure. Error State: $error_state, Error message : $error_msg";
return $class->new (
message => $error_msg,
state => $error_state,
code => $error_code,
);
}
1;

View file

@ -0,0 +1,7 @@
package errors::invalidInput;
use Moo;
extends 'errors::Error';
1;

View file

@ -0,0 +1,7 @@
package errors::invalidObject;
use Moo;
extends 'errors::Error';
1;

View file

@ -0,0 +1,7 @@
package errors::lostResource;
use Moo;
extends 'errors::Error';
1;

7
lib/errors/notFound.pm Normal file
View file

@ -0,0 +1,7 @@
package errors::notFound;
use Moo;
extends 'errors::Error';
1;

View file

@ -0,0 +1,34 @@
package homepage_controller;
use Template;
use Dancer2 appname => 'ProdDashboard';
use Data::Dumper;
use strict;
use warnings;
use TryCatch;
use production::production_helper;
use DateTime;
use DateTime::Format::Strptime;
use utils::ajax::response_helper;
use POSIX ();
sub showHomepage {
my $error;
try {
print "\n\n Before server list print \n\n";
my $servers_list = production_helper::getAllProdServers();
print "\n\n Before server list print \n\n";
print Dumper $servers_list;
my $local_domain = config->{domain};
return template 'homepage/homepage', {
servers_list => $servers_list,
domain => $local_domain
};
} catch ($error){
die $error;
};
}
1;

View file

@ -0,0 +1,160 @@
package production_db;
use Dancer2 appname => 'ProdDashboard';
use Dancer2::Plugin::Database;
use TryCatch;
use Data::Dumper;
use errors::dbError;
sub registerServer {
my ($server_id, $status) = @_;
my $sth;
my $ser_id;
try {
$sth = database->prepare("select * from register_prod_server (?, ?)");
$sth->execute( $server_id, $status );
$ser_id = $sth->fetchrow_array;
} catch {
errors::dbError->from_handle($sth, 'registerServer')->throw();
}
return $ser_id;
}
sub updateServer {
my ($server_id, $status) = @_;
my $sth;
my $ser_id;
try {
$sth = database->prepare("select * from register_prod_server (?, ?)");
$sth->execute( $server_id, $status );
$ser_id = $sth->fetchrow_array;
} catch {
errors::dbError->from_handle($sth, 'updateServer')->throw();
}
return $ser_id;
}
sub getAllProdServers{
my $sth;
my $servers_list;
try {
$sth = database->prepare("select * from get_all_prod_servers ()");
$sth->execute();
$servers_list = $sth->fetchall_arrayref({});
} catch {
errors::dbError->from_handle($sth, 'getAllProdServers')->throw();
}
return $servers_list;
}
sub addLogEntry{
my ($hub_number, $activity, $message, $channel_id, $file_name, $file_size) = @_;
my $sth;
my $log_record_id;
try {
$sth = database->prepare("select * from create_otp_log_entry (?, ?, ?, ?, ?, ?)");
$sth->execute( $hub_number, $activity, $message, $channel_id, $file_name, $file_size );
$log_record_id = $sth->fetchrow_array;
} catch {
errors::dbError->from_handle($sth, 'addLogEntry')->throw();
}
return $log_record_id;
}
# Deletes all channels for single HUB ID passed
sub deleteAllChannels{
my $hub_id = shift;
my $sth;
my $node_id;
try {
$sth = database->prepare("select * from delete_node_channels (?)");
$sth->execute( $hub_id );
} catch {
errors::dbError->from_handle($sth, 'deleteAllChannels')->throw();
}
return;
}
# Adds a channel record in otp_channels table
sub addNodeChannel{
my ($hub_id, $channel_id, $destination_id, $RAG_status, $channel_name) = @_;
my $sth;
my $new_channel_id;
try {
$sth = database->prepare("select * from add_node_channel (?, ?, ?, ?, ?)");
$sth->execute( $hub_id, $channel_id, $destination_id, $RAG_status, $channel_name );
$new_channel_id = $sth->fetchrow_array;
} catch {
errors::dbError->from_handle($sth, 'addNodeConfig')->throw();
}
return $new_channel_id;
}
# gets list of all channels for passed hub id
sub getAllChannels{
my $hub_id = shift;
my $sth;
my $channels_list;
try {
$sth = database->prepare("select * from get_all_otp_channels ( ? )");
$sth->execute( $hub_id );
$channels_list = $sth->fetchall_arrayref({});
} catch {
errors::dbError->from_handle($sth, 'getAllChannels')->throw();
}
return $channels_list;
}
# update file_name field in channels table with passed file_name
# file name is only populated if file is in progress (being transferred) for that channel
# otherwise field is kept empty
sub updateChannelTransferActivity{
my($hub_id, $channel_id, $file_name) = @_;
my $sth;
try {
$sth = database->prepare("select * from channel_update_filename (?, ?, ?)");
$sth->execute( $hub_id, $channel_id, $file_name );
} catch {
errors::dbError->from_handle($sth, 'updateChannelTransferActivity')->throw();
}
return;
}
1;

View file

@ -0,0 +1,154 @@
package production_controller;
use Template;
use Dancer2 appname => 'ProdDashboard';
use Data::Dumper;
use TryCatch;
use utils::ajax::response_helper;
use utils::support::action_helper;
use production::production_helper;
use production::production_logs_helper;
my $GREEN_COLOR = 1;
my $AMBER_COLOR = 2;
my $RED_COLOR = 3;
sub registerServers {
my $error;
try {
my $post_params = params();
my $server_id = $post_params->{'server_id'};
my $status = $post_params->{'status'};
my $registration_id = production_helper::registerServer($server_id, $status);
return ajax_data_response(
message => 'Successful.'
);
} catch($error){
return handleException( $error );
};
}
sub updateServers{
my $error;
try {
my $server_id = param('server_id');
my $status = param('status');
my $message = param('msg');
my $db_status = $AMBER_COLOR; # If status not received, set to AMBER initially.
if(defined $status){
if($status eq "OK"){
$db_status = $GREEN_COLOR;
}else{
$db_status = $RED_COLOR;
production_logs_helper::addLogEntry($server_id, '', $message);
}
}
my $updated_server = production_helper::updateServer($server_id, $db_status);
return ajax_data_response(
message => 'Successful.'
);
} catch($error){
return handleException( $error );
};
}
sub ragUpdate{
my $error;
try {
my $servers_list = production_helper::getAllProdServers();
my $current_time = time();
foreach my $server (@$servers_list){
# Update current server status
serverUpdate( $server, $current_time );
}
production_helper::checkStateChange( $nodes_list );
return ajax_data_response(
message => 'Successful.'
);
} catch($error){
return handleException( $error );
};
}
# Update server and set colour as per time passed
sub serverUpdate{
my ($server, $current_time) = @_;
my $amber_threshold = 3*60; # 3 mins
my $error_threshold = 5*60; # 5 mins
my $time_diff_in_sec = $current_time - $server->{last_active};
if($time_diff_in_sec >= $amber_threshold && $time_diff_in_sec < $error_threshold){
#update Amber status
production_helper::updateServer($server->{id}, $AMBER_COLOR);
production_logs_helper::addLogEntry($server->{id}, 'SERVER_TIMEOUT', 'Timeout Warning');
}elsif($time_diff_in_sec > $error_threshold){
#update Error status
production_helper::updateServer($server->{id}, $RED_COLOR);
production_logs_helper::addLogEntry($server->{id}, 'SERVER_TIMEOUT', 'Timeout Critical');
}
}
sub saveConfig{
my $error;
try {
my $servers_list = config->{servers_list};
foreach my $server_id in (@$servers_list){
# Register servers in the table and mark them down initially
my $registration_id = production_helper::registerServer($server_id, $RED_COLOR);
}
return ajax_data_response(
message => 'Successful.'
);
} catch($error){
return handleException( $error );
};
}
# Update file transfer information in channels and log table
sub updateFileTransferStatus{
my $error;
try {
my $hub_id = param('hubid');
my $channel_id = param('channelid');
my $post_params = params();
my $transfer_status = $post_params->{'transfer_status'};
my $transfer_msg = $post_params->{'transfer_msg'};
my $file_name = $post_params->{'file_name'};
my $file_size = $post_params->{'file_size'};
# add file transfer info in log. Size is in MBs
otp_logs_helper::addLogEntry($hub_id, $transfer_status, $transfer_msg, $channel_id, $file_name, $file_size);
# update filename field in channels table
otp_helper::updateChannelTransferActivity($hub_id, $channel_id, $transfer_status, $file_name);
return ajax_data_response(
message => 'Successful.'
);
} catch($error){
return handleException( $error );
};
}
1;

View file

@ -0,0 +1,96 @@
package production_helper;
use Template;
use Dancer2 appname => 'ProdDashboard';
use Data::Dumper;
use TryCatch;
use production::dao::production_db;
use email::email_helper;
use JSON qw(encode_json decode_json);
use strict;
use warnings;
sub registerServer {
my ($server_id, $status) = @_;
my $res = production_db::registerServer($server_id, $status);
return 1;
}
sub getAllProdServers {
my $servers_list = production_db::getAllProdServers();
return $servers_list;
}
sub updateServer {
my ($server_id, $status) = @_;
my $servers_id = production_db::updateServer($server_id, $status);
return $servers_id;
}
# Check if status for any node/channel is changed from previous state, send emails
sub checkStateChange{
my $nodes_list = shift;
foreach my $node (@$nodes_list){
# send emails for hub status change
sendHubMasterEmails( $node );
# send emails for any status change in hub channels
sendHubChannelEmails( $node );
}
}
# send emails for hub status change
sub sendHubMasterEmails{
my $node = shift;
my $subject = '';
if( $node->{status} != 0 && $node->{status} != $node->{last_status} ){
# send email
if( $node->{status} == 3 ){
$subject = "OTP Node $node->{hub_id}: $node->{hub_name} has gone RED";
}
if( $node->{status} == 1 ){
$subject = "OTP Node $node->{hub_id}: $node->{hub_name} has reverted to GREEN";
}
if( $subject ne '' ){
email_helper::sendMail(config->{sender_email}, config->{admin_email}, $subject, $subject);
}
}
}
# send emails for any status change in hub channels
sub sendHubChannelEmails{
my $node = shift;
my $subject = '';
my $node_channels_list = getAllChannels($node->{hub_id});
foreach my $channel (@$node_channels_list){
if( $channel->{status} != 0 && $channel->{status} != $channel->{last_status} ){
# send channel fail email
if( $channel->{status} == 3 ){
$subject = "OTP Channel $channel->{channel_id} in Node $node->{hub_id}: $node->{hub_name} has gone RED";
}
if( $channel->{status} == 1 ){
$subject = "OTP Channel $channel->{channel_id} in Node $node->{hub_id}: $node->{hub_name} has reverted to GREEN";
}
if( $subject ne '' ){
email_helper::sendMail(config->{sender_email}, config->{admin_email}, $subject, $subject);
}
}
}
}
1;

View file

@ -0,0 +1,18 @@
package production_logs_helper;
use Template;
use Dancer2 appname => 'dashboard';
use Data::Dumper;
use TryCatch;
use production::dao::production_db;
sub addLogEntry {
my ($server_number, $activity, $message) = @_;
my $record_id = production_db::addLogEntry($server_number, $activity, $message);
return $record_id;
}
1;

87
lib/utils/Mailer.pm Normal file
View file

@ -0,0 +1,87 @@
package Mailer;
use Net::SMTP;
use Authen::SASL;
use Time::Local;
use Data::Dumper;
######################################################################
# send_mail
#
# param 1 - from address
# param 2 - either to address, or array of to addresses (use , to to multi)
# param 3 - body of mail
# param 4 - subject
######################################################################
sub send_mail
{
my ($from, $to, $body, $subject, $msg);
my ($SMTP_HOST, $smtp);
my (@to_addr);
$from = $_[0];
$to = $_[1];
$body = $_[2];
$subject = $_[3];
$SMTP_HOST = 'smtp.mailgun.org';
# convert the list of to address to array
@to_addr = split(',', $to);
$msg = "MIME-Version: 1.0\n"
. "From: $from\n"
. "To: " . join(';', @to_addr) . "\n"
. "Subject: $subject\n\n" # Double \n
. $body;
#
# Open a SMTP session
#
$smtp = Net::SMTP->new( $SMTP_HOST,
Hello => 'oliver.solutions',
Debug => 0, # Change to a 1 to turn on debug messages
);
if(!defined($smtp) || !($smtp))
{
print "SMTP ERROR: Unable to open smtp session.\n";
return 0;
}
$smtp->auth('postmaster@oliver.solutions', 'a989aff7fdb39352a0b7b6e3ee4794ed');
$smtp->banner();
$smtp->domain();
#
# Pass the 'from' email address, exit if error
#
if (! ($smtp->mail( $from ) ) )
{
return 0;
}
#
# Pass the recipient address(es)
#
foreach (@to_addr) {
if (! ($smtp->recipient( $_ )))
{
return 0;
}
}
#
# Send the message
#
$smtp->data( $msg );
$smtp->quit;
return 1;
}
# must have this at the end for a require
1;

View file

@ -0,0 +1,23 @@
package utils::ajax::response_helper;
use strict;
use warnings;
use JSON qw(to_json);
use Exporter qw(import);
our @EXPORT = qw(ajax_error_response ajax_data_response);
sub ajax_error_response {
return ajax_json_response('error', {@_});
}
sub ajax_data_response {
return ajax_json_response('data', {@_});
}
sub ajax_json_response {
my ($root, $data) = @_;
return to_json({($root => $data)}, { allow_blessed => 1, convert_blessed => 1 });
}
1;

View file

@ -0,0 +1,62 @@
package WhiteListSanitizer;
use strict;
use warnings;
use HTML::Scrubber;
our @default_allowed_tags = qw(strong em b i p code pre tt samp kbd var sub
sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol
li dl dt dd abbr acronym a img blockquote del ins);
our @default_allowed_attributes = qw(href src width height alt cite datetime
title class name xml:lang abbr);
sub new {
my ($class, @args) = @_;
my $self = bless {}, $class;
return $self->_init(@args);
}
sub _init {
my ($self) = @_;
return $self;
}
sub _scrubber {
my $self = shift;
return $self->{_scrubber} //= HTML::Scrubber->new;
}
sub _allowed_tags {
my ($self, $options) = shift;
$options->{tags} || [@default_allowed_tags];
}
sub _allowed_attributes {
my ($self, $options) = shift;
$options->{attributes} || [@default_allowed_attributes];
}
sub _configure_scrubber {
my ($self, $options) = @_;
$self->_scrubber->allow(map {$_ => 1} @{$self->_allowed_tags($options)});
$self->_scrubber->default(
0 => { # default rule, deny all tags
'*' => 0, # default rule, deny all attributes
map { $_ => 1 } @{$self->_allowed_attributes($options)}
}
);
}
sub sanitize {
my ($self, $html, %options) = @_;
return unless defined $html;
return $html if $html eq "";
$self->_configure_scrubber(\%options);
return $self->_scrubber->scrub($html);
}
1;

View file

@ -0,0 +1,75 @@
package utils::support::action_helper;
use Dancer2 appname => 'ProdDashboard';
use Carp;
use TryCatch;
use Scalar::Util qw(blessed);
use Moo;
use Dancer2::Plugin::Deferred;
use utils::ajax::response_helper;
use utils::support::WhiteListSanitizer;
use Exporter qw(import);
our @EXPORT = qw(
redirect_to
sanitize
handleException
);
our @EXPORT_OK = qw();
sub sanitize {
return WhiteListSanitizer->new->sanitize(@_);
}
sub redirect_to {
my ($path, %args) = @_;
while (my ($key, $value) = each %args) {
deferred $key => $value;
}
return redirect $path;
}
sub handleException{
my ($error, $target) = @_;
my $msg;
# check fo Moo based error objects
if (blessed $error){
if ( $error->isa('errors::AccessDenied') or $error->isa('errors::ActivityDenied') ) {
$msg = 'Access to this resource has been denied.';
}
elsif ($error->isa('errors::dbError') and ($error->type() eq 'UniqueViolation')) {
$msg = "Record already exists."
}
elsif ($error->isa('errors::dbError')) {
$msg = "Your data was not saved successfully! Please try again later."
}
else{
$msg = $error->{message};
}
if(defined $error->{target}){
$target = $error->{target};
}
}
else{
$msg = 'Something went wrong.';
}
if(defined $target && $target ne ""){
deferred error => $msg;
redirect $target;
}
else{
return ajax_error_response(
message => $msg
);
}
}
1;

View file

@ -0,0 +1,82 @@
package utils::support::moo_sanitize;
use strict;
use warnings;
use Carp;
use Scalar::Util qw(blessed);
use utils::support::WhiteListSanitizer;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT = qw(sanitizes sanitize);
my %THINGS = ();
my $_sanitizer;
sub sanitizer {
return $_sanitizer //= WhiteListSanitizer->new;
}
sub sanitizes {
my ($fields, %args) = @_;
my $caller = caller;
my $thing = $THINGS{$caller};
$fields = [$fields] if ref($fields) ne 'ARRAY';
my $sanitize_options = %args ? \%args : 1;
foreach my $field (@$fields) {
$thing->{fields}->{$field} = $sanitize_options;
}
return;
}
sub sanitize {
my $self = shift;
my $class = blessed $self;
my $thing = $THINGS{$class};
while (my ($field, $options) = each %{$thing->{fields}}) {
my $value = $self->$field;
my %santize_options;
if (ref($options) eq "HASH") {
%santize_options = %$options;
}
$self->$field(sanitizer->sanitize($value, %santize_options));
}
return;
}
sub import {
my $class = shift;
my $caller = caller;
my $thing = $THINGS{$caller} = {
fields => {}
};
no strict 'refs';
no warnings 'redefine';
my $import = $caller->can('import');
*{"$caller\::import"} = sub {
foreach my $field (keys %{$thing->{fields}}) {
if (not $caller->can($field)) {
croak "Field: $field is not available on $caller";
}
}
if ($import) {
$import->(@_);
}
};
__PACKAGE__->export_to_level(1, @_);
}
1;

View file

@ -0,0 +1,341 @@
package utils::support::moo_validations;
use strict;
use warnings;
use Carp;
use Scalar::Util qw(blessed);
use Scalar::Util::Numeric qw(:all);
use Switch;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT = qw(validates is_valid errors);
our @EXPORT_OK = qw(register_validator);
my %VALIDATORS = (
presence => \&_presense_validator,
numericality => \&_numericality_validator,
length => \&_length_validator,
#with => \&_with_validator;
);
my %THINGS = ();
sub validates {
my ($fields, %args) = @_;
my $caller = caller;
my $thing = $THINGS{$caller};
$fields = [$fields] if ref($fields) ne 'ARRAY';
my $all_on = delete $args{on};
my $all_message = delete $args{message};
my $all_allow_blank = delete $args{allow_blank};
foreach my $field (@$fields) {
my $field_validators = $thing->{fields}->{$field} //= [];
while (my ($validator, $options) = each %args) {
if (not exists $VALIDATORS{$validator}) {
croak "There is no validator registered with name $validator";
}
if (not ref($options) eq 'HASH') {
my $option = $options;
$options = {};
if (ref($option) eq 'ARRAY') {
$options->{in} = $option;
} else {
$options->{with} = $option;
}
}
my $on = delete $options->{on} || $all_on || 'all';
my $message = delete $options->{message} || $all_message;
my $allow_blank = delete $options->{allow_blank} || $all_allow_blank;
my $validation_options = {
on => $on,
message => $message,
allow_blank => $allow_blank
};
push @$field_validators, {
validator => $VALIDATORS{$validator},
args => $options,
options => $validation_options
};
}
}
return;
}
sub is_valid {
my ($self, @args) = @_;
my $class = blessed $self;
if (@args % 2 != 0) {
my ($field, %args) = @args;
return _validate_field($self, $field, %args);
}
my $thing = $THINGS{$class};
my $valid = 1;
$self->{_validation_errors} = {};
foreach my $field (keys %{$thing->{fields}}) {
my $field_valid = _validate_field($self, $field, @args);
$valid &&= $field_valid;
}
return $valid;
}
sub _validate_field {
my ($self, $field, %args) = @_;
my $class = blessed $self;
my $thing = $THINGS{$class};
my $field_valid = 1;
my $for = $args{for} || 'all';
my $field_validators = $thing->{fields}->{$field} // [];
foreach my $validation (@$field_validators) {
my $validator = $validation->{validator};
my $validator_args = $validation->{args};
my $options = $validation->{options};
my $on = $options->{on};
my $message = $options->{message};
my $allow_blank = $options->{allow_blank};
my $validator_messages = [];
next unless ($on eq 'all' || $on eq $for);
my $field_value = $self->$field;
next if ($allow_blank && (not defined $field_value || $field_value eq ""));
my $valid = $validator->(
$self->$field,
%$validator_args,
messages => $validator_messages
);
if (not $valid) {
$self->errors($field => $message || $validator_messages);
$field_valid = 0;
}
}
return $field_valid;
}
sub errors {
my ($self, %args) = @_;
my $class = blessed $self;
$self->{_validation_errors} //= {};
if (keys %args == 0) {
return $self->{_validation_errors};
}
while (my ($field, $error_messages) = each %args) {
my $field_errors = $self->{_validation_errors}->{$field} //= [];
$error_messages = [$error_messages] if ref($error_messages) ne 'ARRAY';
push @$field_errors, @$error_messages;
}
return;
}
sub register_validator {
my ($name, $sub) = @_;
if (not ref($sub) eq 'CODE') {
croak 'Validator subroutine must be a CODE ref';
}
if (exists $VALIDATORS{$name}) {
carp "A Validator subroutine with name $name already exists";
}
$VALIDATORS{$name} = $sub;
return;
}
sub _presense_validator {
my ($value, %config) = @_;
my $required = $config{with} || $config{is} || 1;
my $present = defined $value && $value ne "";
my $pass = $required == $present;
if (not $pass) {
push @{$config{messages}}, " must be present.";
}
return $pass;
}
sub _numericality_validator {
my ($value, %config) = @_;
my $messages = delete $config{messages};
my $only_integer = !! delete $config{only_integer};
if ($only_integer) {
unless (isint($value)) {
push @$messages, " is not an integer";
return 0;
}
} else {
my $numericality = !! delete $config{with} || 1;
unless (isnum($value) == $numericality) {
push @$messages, $numericality ? " is not a number" : " is a number";
return 0;
}
}
my %numericality_options = %config;
my $pass = 1;
while (my ($option, $option_value) = each %numericality_options) {
unless ($option eq 'odd' || $option eq 'even') {
croak "$option must be a number" if (not isnum($option_value));
}
switch ($option) {
case 'greater_than' {
unless ($value > $option_value) {
push @$messages, " is not greater than $option_value";
$pass = 0;
}
}
case 'greater_than_or_equal_to' {
unless ($value >= $option_value) {
push @$messages, " is not greater than or equal to $option_value";
$pass = 0;
}
}
case 'equal_to' {
unless ($value == $option_value) {
push @$messages, " is not equal to $option_value";
$pass = 0;
}
}
case 'less_than' {
unless ($value < $option_value) {
push @$messages, " is not less than $option_value";
$pass = 0;
}
}
case 'less_than_or_equal_to' {
unless ($value <= $option_value) {
push @$messages, " is not less than or equal to $option_value";
$pass = 0;
}
}
case 'odd' {
unless ($value % 2 == 1) {
push @$messages, " is not a odd number";
$pass = 0;
}
}
case 'even' {
unless ($value % 2 == 0) {
push @$messages, " is not a even number";
$pass = 0;
}
}
}
}
return $pass;
}
sub _length_validator {
my ($value, %config) = @_;
my $messages = delete $config{messages};
my ($option) = keys %config;
my $option_value = $config{$option};
my $value_length = length $value;
my $pass = 1;
switch ($option) {
case ['within', 'in'] {
my ($min, $max) = @$option_value;
if ($value_length < $min || $value_length > $max) {
push @$messages, " is the wrong length (should be between $min and $max characters)";
$pass = 0;
}
}
case 'is' {
if ($value_length != $option_value) {
push @$messages, " is the wrong length (should be $option_value characters)";
$pass = 0;
}
}
case 'minimum' {
if ($value_length < $option_value) {
push @$messages, " is too short (minimum is $option_value characters)";
$pass = 0;
}
}
case 'maxmimum' {
if ($value_length > $option_value) {
push @$messages, " is too long (maximum is $option_value characters)";
$pass = 0;
}
}
}
return $pass;
}
sub import {
my $class = shift;
my $caller = caller;
my $thing = $THINGS{$caller} = {
fields => {}
};
no strict 'refs';
no warnings 'redefine';
my $import = $caller->can('import');
*{"$caller\::import"} = sub {
# Attempt to check at compile time
# if validate fields exists on object
foreach my $field (keys %{$thing->{fields}}) {
if (not $caller->can($field)) {
croak "Field: $field is not available on $caller";
}
}
if ($import) {
$import->(@_);
}
};
__PACKAGE__->export_to_level(1, @_);
}
1;

1
prun.sh Normal file
View file

@ -0,0 +1 @@
plackup -s Starman bin/app.psgi

18
public/404.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Error 404</title>
<link rel="stylesheet" href="/css/error.css">
</head>
<body>
<h1>Error 404</h1>
<div id="content">
<h2>Page Not Found</h2><p>Sorry, this is the void.</p>
</div>
<div id="footer">
Powered by <a href="http://perldancer.org/">Dancer2</a>.
</div>
</body>
</html>

18
public/500.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Error 500</title>
<link rel="stylesheet" href="/css/error.css">
</head>
<body>
<h1>Error 500</h1>
<div id="content">
<h2>Internal Server Error</h2><p>Wooops, something went wrong</p>
</div>
<div id="footer">
Powered by <a href="http://perldancer.org/">Dancer2</a>.
</div>
</body>
</html>

85
public/css/error.css Normal file
View file

@ -0,0 +1,85 @@
body {
font-family: Lucida,sans-serif;
}
h1 {
color: #AA0000;
border-bottom: 1px solid #444;
}
h2 { color: #444; }
pre {
font-family: "lucida console","monaco","andale mono","bitstream vera sans mono","consolas",monospace;
font-size: 12px;
border-left: 2px solid #777;
padding-left: 1em;
}
footer {
font-size: 10px;
}
span.key {
color: #449;
font-weight: bold;
width: 120px;
display: inline;
}
span.value {
color: #494;
}
/* these are for the message boxes */
pre.content {
background-color: #eee;
color: #000;
padding: 1em;
margin: 0;
border: 1px solid #aaa;
border-top: 0;
margin-bottom: 1em;
overflow-x: auto;
}
div.title {
font-family: "lucida console","monaco","andale mono","bitstream vera sans mono","consolas",monospace;
font-size: 12px;
background-color: #aaa;
color: #444;
font-weight: bold;
padding: 3px;
padding-left: 10px;
}
table.context {
border-spacing: 0;
}
table.context th, table.context td {
padding: 0;
}
table.context th {
color: #889;
font-weight: normal;
padding-right: 15px;
text-align: right;
}
.errline {
color: red;
}
pre.error {
background: #334;
color: #ccd;
padding: 1em;
border-top: 1px solid #000;
border-left: 1px solid #000;
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
}

189
public/css/style.css Normal file
View file

@ -0,0 +1,189 @@
body {
margin: 0;
margin-bottom: 25px;
padding: 0;
background-color: #ddd;
background-image: url("/images/perldancer-bg.jpg");
background-repeat: no-repeat;
background-position: top left;
font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
font-size: 13px;
color: #333;
}
h1 {
font-size: 28px;
color: #000;
}
a {color: #03c}
a:hover {
background-color: #03c;
color: white;
text-decoration: none;
}
#page {
background-color: #ddd;
width: 750px;
margin: auto;
margin-left: auto;
padding-left: 0px;
margin-right: auto;
}
#content {
background-color: white;
border: 3px solid #aaa;
border-top: none;
padding: 25px;
width: 500px;
}
#sidebar {
float: right;
width: 175px;
}
#header, #about, #getting-started {
padding-left: 75px;
padding-right: 30px;
}
#header {
background-image: url("/images/perldancer.jpg");
background-repeat: no-repeat;
background-position: top left;
height: 64px;
}
#header h1, #header h2 {margin: 0}
#header h2 {
color: #888;
font-weight: normal;
font-size: 16px;
}
#about h3 {
margin: 0;
margin-bottom: 10px;
font-size: 14px;
}
#about-content {
background-color: #ffd;
border: 1px solid #fc0;
margin-left: -11px;
}
#about-content table {
margin-top: 10px;
margin-bottom: 10px;
font-size: 11px;
border-collapse: collapse;
}
#about-content td {
padding: 10px;
padding-top: 3px;
padding-bottom: 3px;
}
#about-content td.name {color: #555}
#about-content td.value {color: #000}
#about-content.failure {
background-color: #fcc;
border: 1px solid #f00;
}
#about-content.failure p {
margin: 0;
padding: 10px;
}
#getting-started {
border-top: 1px solid #ccc;
margin-top: 25px;
padding-top: 15px;
}
#getting-started h1 {
margin: 0;
font-size: 20px;
}
#getting-started h2 {
margin: 0;
font-size: 14px;
font-weight: normal;
color: #333;
margin-bottom: 25px;
}
#getting-started ol {
margin-left: 0;
padding-left: 0;
}
#getting-started li {
font-size: 18px;
color: #888;
margin-bottom: 25px;
}
#getting-started li h2 {
margin: 0;
font-weight: normal;
font-size: 18px;
color: #333;
}
#getting-started li p {
color: #555;
font-size: 13px;
}
#search {
margin: 0;
padding-top: 10px;
padding-bottom: 10px;
font-size: 11px;
}
#search input {
font-size: 11px;
margin: 2px;
}
#search-text {width: 170px}
#sidebar ul {
margin-left: 0;
padding-left: 0;
}
#sidebar ul h3 {
margin-top: 25px;
font-size: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
#sidebar li {
list-style-type: none;
}
#sidebar ul.links li {
margin-bottom: 5px;
}
h1, h2, h3, h4, h5 {
font-family: sans-serif;
margin: 1.2em 0 0.6em 0;
}
p {
line-height: 1.5em;
margin: 1.6em 0;
}
code, .filepath, .app-info {
font-family: 'Andale Mono', Monaco, 'Liberation Mono', 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', monospace;
}
#footer {
clear: both;
padding-top: 2em;
text-align: center;
padding-right: 160px;
font-family: sans-serif;
font-size: 10px;
}

View file

@ -0,0 +1,63 @@
table {
color: #333; /* Lighten up font color */
font-family: Helvetica, Arial, sans-serif; /* Nicer font */
width: 690px;
border-collapse:
collapse; border-spacing: 0;
margin: auto;
}
td, th { border: 1px solid #CCC; height: 30px; } /* Make cells a bit taller */
th {
background: #F3F3F3; /* Light grey background */
font-weight: bold; /* Make sure they're bold */
}
td {
background: #FAFAFA; /* Lighter grey background */
text-align: center; /* Center our text */
}
td.red{
background: red;
}
td.green{
background: green;
}
td.amber{
background: #FFC200;
}
.form-control.datefield{
width: 120px;
background: #fafafa none repeat scroll 0 0;
border: none;
font-family: Helvetica,Arial,sans-serif;
font-size: 14px;
color: #333;
}
.container-fluid {
height: 100%;
}
.clickable-row{
cursor: pointer;
}
.button{
display: block;
width: 115px;
height: 25px;
background: #b2b2b2;
padding: 10px;
text-align: center;
border-radius: 5px;
color: white;
font-weight: bold;
margin: 20px auto auto;
text-decoration: none;
}

16
public/dispatch.cgi Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env perl
BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';}
use Dancer2;
use FindBin '$RealBin';
use Plack::Runner;
# For some reason Apache SetEnv directives don't propagate
# correctly to the dispatchers, so forcing PSGI and env here
# is safer.
set apphandler => 'PSGI';
set environment => 'production';
my $psgi = path($RealBin, '..', 'bin', 'app.psgi');
die "Unable to read startup script: $psgi" unless -r $psgi;
Plack::Runner->run($psgi);

18
public/dispatch.fcgi Normal file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env perl
BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';}
use Dancer2;
use FindBin '$RealBin';
use Plack::Handler::FCGI;
# For some reason Apache SetEnv directives don't propagate
# correctly to the dispatchers, so forcing PSGI and env here
# is safer.
set apphandler => 'PSGI';
set environment => 'production';
my $psgi = path($RealBin, '..', 'bin', 'app.psgi');
my $app = do($psgi);
die "Unable to read startup script: $@" if $@;
my $server = Plack::Handler::FCGI->new(nproc => 5, detach => 1);
$server->run($app);

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,9 @@
$( document ).ready(function() {
$(".clickable-row").click(function() {
// window.location = $(this).data("href");
});
});

4
public/javascripts/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

5
t/001_base.t Normal file
View file

@ -0,0 +1,5 @@
use strict;
use warnings;
use Test::More tests => 1;
use_ok 'ProdDashboard';

16
t/002_index_route.t Normal file
View file

@ -0,0 +1,16 @@
use strict;
use warnings;
use ProdDashboard;
use Test::More tests => 2;
use Plack::Test;
use HTTP::Request::Common;
use Ref::Util qw<is_coderef>;
my $app = ProdDashboard->to_app;
ok( is_coderef($app), 'Got app' );
my $test = Plack::Test->create($app);
my $res = $test->request( GET '/' );
ok( $res->is_success, '[GET /] successful' );

View file

@ -0,0 +1,43 @@
<%
page = "homepage"
%>
<style>
.blue_background{
background-color: #87CEFA;
}
</style>
<% USE date %>
<script type="text/javascript" src="/javascripts/homepage/homepage.js"></script>
<meta http-equiv="refresh" content="10; URL=<% domain %>">
<table class="table-pagecentered">
<thead>
<tr>
<th>Server Name</th>
<th>Last Active</th>
<th>RAG Status</th>
</tr>
</thead>
<tbody>
<% FOREACH prod_ser IN servers_list %>
<tr class='clickable-row' data-href=''>
<td class = "" ><% prod_ser.server_number %></td>
<td class = "" >
<% date.format(prod_ser.last_active, '%Y-%m-%d %H:%M') %>
</td>
<% IF prod_ser.status == 1 %>
<td class='green'>&nbsp;</td>
<% ELSIF prod_ser.status == 2 %>
<td class='amber'>&nbsp;</td>
<% ELSE %>
<td class='red'>&nbsp;</td>
<% END %>
</tr>
<% END %>
</tbody>
</table>

148
views/index.tt Normal file
View file

@ -0,0 +1,148 @@
<!--
Credit goes to the Ruby on Rails team for this page
has been heavily based on the default Rails page that is
built with a scaffolded application.
Thanks a lot to them for their work.
See Ruby on Rails if you want a kickass framework in Ruby:
http://www.rubyonrails.org/
-->
<div id="page">
<div id="sidebar">
<ul id="sidebar-items">
<li>
<h3>Join the community</h3>
<ul class="links">
<li><a href="http://perldancer.org/">PerlDancer</a></li>
<li><a href="http://twitter.com/PerlDancer/">Official Twitter</a></li>
<li><a href="https://github.com/PerlDancer/Dancer2/">GitHub Community</a></li>
</ul>
</li>
<li>
<h3>Browse the documentation</h3>
<ul class="links">
<li><a
href="https://metacpan.org/pod/Dancer2::Manual">Introduction</a></li>
<li><a href="https://metacpan.org/pod/Dancer2::Cookbook">Cookbook</a></li>
<li><a
href="https://metacpan.org/pod/Dancer2::Tutorial"
title="a tutorial to build a small blog engine with Dancer">Tutorial</a></li>
</ul>
</li>
<li>
<h3>Your application's environment</h3>
<ul>
<li>Location: <span class="filepath">/var/www/ProdDashboard</span></li>
<li>Template engine: <span class="app-info"><% settings.template %></span></li>
<li>Logger: <span class="app-info"><% settings.logger %></span></li>
<li>Environment: <span class="app-info"><% settings.environment %></span></li>
</ul>
</li>
</ul>
</div>
<div id="content">
<div id="header">
<h1>Perl is dancing</h1>
<h2>You&rsquo;ve joined the dance floor!</h2>
</div>
<div id="getting-started">
<h1>Getting started</h1>
<h2>Here&rsquo;s how to get dancing:</h2>
<h3><a href="#" id="about_env_link">About your application's environment</a></h3>
<div id="about-content" style="display: none;">
<table>
<tbody>
<tr>
<td>Perl version</td>
<td><span class="app-info"><% perl_version %></span></td>
</tr>
<tr>
<td>Dancer2 version</td>
<td><span class="app-info"><% dancer_version %></span></td>
</tr>
<tr>
<td>Backend</td>
<td><span class="app-info"><% settings.apphandler %></span></td>
</tr>
<tr>
<td>Appdir</td>
<td><span class="filepath">/var/www/ProdDashboard</span></td>
</tr>
<tr>
<td>Template engine</td>
<td><span class="app-info"><% settings.template %></span></td>
</tr>
<tr>
<td>Logger engine</td>
<td><span class="app-info"><% settings.logger %></span></td>
</tr>
<tr>
<td>Running environment</td>
<td><span class="app-info"><% settings.environment %></span></td>
</tr>
</tbody>
</table>
</div>
<script type="text/javascript">
$('#about_env_link').click(function() {
$('#about-content').slideToggle('fast', function() {
// ok
});
return false;
});
</script>
<ol>
<li>
<h2>Tune your application</h2>
<p>
Your application is configured via a global configuration file,
<span class="filepath">config.yml</span> and an "environment" configuration file,
<span class="filepath">environments/development.yml</span>. Edit those files if you
want to change the settings of your application.
</p>
</li>
<li>
<h2>Add your own routes</h2>
<p>
The default route that displays this page can be removed,
it's just here to help you get started. The template used to
generate this content is located in
<span class="filepath">views/index.tt</span>.
You can add some routes to <span class="filepath">lib/ProdDashboard.pm</span>.
</p>
</li>
<li>
<h2>Enjoy web development again</h2>
<p>
Once you've made your changes, restart your standalone server
<span class="filepath">(bin/app.psgi)</span> and you're ready
to test your web application.
</p>
</li>
</ol>
</div>
</div>
</div>

54
views/layouts/main.tt Normal file
View file

@ -0,0 +1,54 @@
<%
application_name = "Production Dashboard"
root_path = request.uri_base
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=<% settings.charset %>" />
<title><% application_name %></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/custom/default/images/favicon.ico">
<link type="text/css" rel="stylesheet" href="/custom/default/app_dashboard.css">
<script type="text/javascript" src="/javascripts/jquery.js"></script>
<!--LIBRARY JAVASCRIPT-->
<!-- <script type="text/javascript" src="/common/javascript/javascript_v2.js"></script>
<script type="text/javascript" src="/common/javascript/jquery/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="/common/javascript/jquery/jquery-migrate-1.2.1.js"></script>
<script type="text/javascript" src="/common/javascript/bootstrap/bootstrap.min.js"></script>
<script type="text/javascript" src="/common/javascript/remoteChained/jquery.chained.remote.min.js"></script>
<script type="text/javascript" src="/common/javascript/bootstrapDatePicker/moment-with-locales.js"></script>
<script type="text/javascript" src="/common/javascript/bootstrapDatePicker/bootstrap-datetimepicker.js"></script> -->
<!--CUSTOM JAVASCRIPT-->
<!-- <script type="text/javascript" src="/javascripts/navigation/navigation_omg.js"></script>
<script type="text/javascript" src="/javascripts/generic/dates_omg.js"></script>
<script type="text/javascript" src="/javascripts/generic/password_modal_omg.js"></script>
<script type="text/javascript" src="/javascripts/alerts/flash_alert_omg.js"></script> -->
<!-- <link rel="stylesheet" href="/common/css/jquery/jquery-ui.css" type="text/css" media="all" />
<link rel="stylesheet" href="/common/css/datePicker/datepicker.min.css" type="text/css" media="all" />
<link rel="stylesheet" href="/common/css/datePicker/datepicker3.min.css" type="text/css" media="all" />
<link rel="stylesheet" href="/css/main_omg.css" type="text/css" media="all" /> -->
</head>
<body>
<div class="container-fluid">
<!--START CONTENT-->
<% content %>
<!--END CONTENT-->
</div>
</body>
</html>