Merge pull request #755 from statping/checkin-updates

v0.90.61 - SASS templating, Checkin updates (backend and UI), UI fixes
pull/753/head
Hunter Long 2020-07-20 13:29:13 -07:00 committed by GitHub
commit 6e26c7c6c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 997 additions and 678 deletions

3
.github/FUNDING.yml vendored
View File

@ -1,3 +1,4 @@
github: hunterlong
patreon: statping
custom: ['https://www.nfoservers.com/donate.pl?force_recipient=1&recipient=info%40socialeck.com', 'https://opencollective.com/statping', 'https://www.buymeacoffee.com/hunterlong']
open_collective: statping
custom: ['https://www.nfoservers.com/donate.pl?force_recipient=1&recipient=info%40socialeck.com', 'https://www.buymeacoffee.com/hunterlong']

View File

@ -328,12 +328,12 @@ jobs:
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
id: buildx-docker-master
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
key: buildx-docker-master
restore-keys: |
${{ runner.os }}-buildx-
buildx-docker-master
- name: Docker Build :base
run: make buildx-base

View File

@ -428,12 +428,12 @@ jobs:
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
id: buildx-docker
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
key: buildx-docker
restore-keys: |
${{ runner.os }}-buildx-
buildx-docker
- name: Docker Build :base
run: make buildx-base

View File

@ -1,3 +1,10 @@
# 0.90.61 (07-19-2020)
- Modified sass layouts, organized and split up sections
- Modified Checkins to seconds rather than milliseconds (for cronjob)
- Modified Service View page to show data inside cards
- Fixed issue with uptime_data sending incorrect start/end timestamps
- Modified http cache to bypass if url has a "v" query param
# 0.90.60 (07-15-2020)
- Added LETSENCRYPT_ENABLE (boolean) env to enable/disable letsencrypt SSL

View File

@ -353,5 +353,5 @@ buildx-base: multiarch
multiarch:
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
.PHONY: all build multiarch build-all buildx-base buildx-dev buildx-latest build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.PHONY: all build certs multiarch build-all buildx-base buildx-dev buildx-latest build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.SILENT: travis_s3_creds

11
dev/postman.json vendored
View File

@ -4911,8 +4911,7 @@
" var first = jsonData[0];",
" var id = pm.globals.get(\"checkin_id\");",
" pm.expect(first.name).to.eql(\"Demo Checkin 1\");",
" pm.expect(first.grace).to.eql(300);",
" pm.expect(first.interval).to.eql(300);",
" pm.expect(first.interval).to.eql(3);",
"});"
],
"type": "text/javascript"
@ -4990,8 +4989,7 @@
" pm.expect(jsonData.status).to.eql(\"success\");",
" pm.expect(jsonData.type).to.eql(\"checkin\");",
" pm.expect(jsonData.output.name).to.eql(\"Server Checkin\");",
" pm.expect(jsonData.output.grace).to.eql(60);",
" pm.expect(jsonData.output.interval).to.eql(900);",
" pm.expect(jsonData.output.interval).to.eql(3);",
" var id = jsonData.output.api_key;",
" pm.globals.set(\"checkin_id\", id);",
"});"
@ -5022,7 +5020,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"service_id\": 2,\n \"name\": \"Server Checkin\",\n \"interval\": 900,\n \"grace\": 60\n}",
"raw": "{\n \"service_id\": 2,\n \"name\": \"Server Checkin\",\n \"interval\": 3\n}",
"options": {
"raw": {}
}
@ -5182,8 +5180,7 @@
" var id = pm.globals.get(\"checkin_id\");",
" pm.expect(jsonData.name).to.eql(\"Server Checkin\");",
" pm.expect(jsonData.api_key).to.eql(id);",
" pm.expect(jsonData.grace).to.eql(60);",
" pm.expect(jsonData.interval).to.eql(900);",
" pm.expect(jsonData.interval).to.eql(3);",
"});"
],
"type": "text/javascript"

View File

@ -113,10 +113,6 @@ HTML, BODY {
color: #fff;
}
.nav-pills .nav-link {
border-radius: 0.2rem;
}
.form-control {
border-radius: 0.2rem;
}

View File

@ -155,6 +155,10 @@ class Api {
return axios.delete('api/incidents/'+incident.id).then(response => (response.data))
}
async checkin(api) {
return axios.get('api/checkins/'+api).then(response => (response.data))
}
async checkin_create(data) {
return axios.post('api/checkins', data).then(response => (response.data))
}

View File

@ -1,8 +1,5 @@
@import 'variables';
HTML,BODY {
background-color: $background-color;
}
@import 'mixin';
.index-chart {
height: $service-card-height;
@ -26,26 +23,11 @@ HTML,BODY {
box-shadow: 0px 3px 6px 1px rgba(0,0,0,0.08);
}
.copy-btn {
position: absolute;
right: 0;
}
.btn-xs {
font-size: 8pt;
padding: 2px 6px;
}
.copy-btn BUTTON {
background-color: white;
margin: 6px;
height: 26px;
font-size: 8pt;
padding: 5px 7px;
border: 1px solid #a7a7a7;
border-radius: 4px !important;
}
.dim {
background-color: #f3f3f3;
}
@ -55,41 +37,6 @@ HTML,BODY {
font-weight: bold;
}
/* The slider itself */
.slider {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 5px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
}
/* Mouse-over effects */
.slider:hover {
opacity: 1; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
border-radius: 50%;
width: 20px; /* Set a specific slider handle width */
height: 20px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.slider::-moz-range-thumb {
width: 15px; /* Set a specific slider handle width */
height: 15px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
@-o-keyframes fadeIt {
0% { background-color: #f5f5f5; }
50% { background-color: #f2f2f2; }
@ -151,18 +98,6 @@ HTML,BODY {
box-shadow: none;
}
.contain-card {
.card-header {
font-size: 1.15rem;
}
.dropdown-menu {
background-color: rgba(239, 239, 239, 0.65);
}
}
.dropup .dropdown-menu {
border-radius: 8px 8px 8px 0;
background-color: #efefef;
@ -202,13 +137,6 @@ HTML,BODY {
opacity: 0;
}
.container {
padding-top: 20px;
padding-bottom: 25px;
max-width: $max-width;
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.header-title {
color: $title-color;
}
@ -226,10 +154,6 @@ HTML,BODY {
padding: 5px 7px;
}
.navbar {
margin-bottom: 30px;
}
.btn-sm {
line-height: 1.3;
font-size: 0.75rem;
@ -274,20 +198,6 @@ HTML,BODY {
color: $service-description-color
}
.footer {
text-decoration: none;
margin-top: 20px;
}
.footer A {
color: $footer-text-color;
text-decoration: none;
}
.footer A:HOVER {
color: #6d6d6d;
}
.font-0 {
font-size: 0.35rem;
}
@ -342,22 +252,6 @@ HTML,BODY {
}
}
.card-body .badge {
color: #fff;
}
.nav-pills .nav-link {
border-radius: $global-border-radius;
}
.form-control {
border-radius: $global-border-radius;
}
.mini_success {
background-color: #f3f3f3;
}
.no-decoration {
color: black;
text-decoration: none;
@ -368,10 +262,6 @@ HTML,BODY {
text-decoration: none;
}
.mini_error {
background-color: #ffbbbb;
}
.btn-white {
background-color: white;
border: 1px solid #d8d8d8;
@ -418,27 +308,6 @@ HTML,BODY {
font-weight: bold;
}
.card {
background-color: $service-background;
border: $service-border;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.08);
}
.card-body {
overflow: hidden;
}
.card-body H4 A {
color: $service-title;
text-decoration: none;
}
.card-title A {
color: $service-title;
text-decoration: none;
}
.chart-container {
position: relative;
height: 240px;
@ -458,79 +327,6 @@ HTML,BODY {
width: 100%;
}
.inputTags-field {
border: 0;
background-color: transparent;
padding-top: .13rem;
}
input.inputTags-field:focus {
outline-width: 0;
}
.inputTags-list {
display: block;
width: 100%;
min-height: calc(2.25rem + 2px);
padding: .2rem .35rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.inputTags-item {
background-color: #3aba39;
margin-right: 5px;
padding: 5px 8px;
font-size: 10pt;
color: white;
border-radius: 4px;
}
.inputTags-item .close-item {
margin-left: 6px;
font-size: 13pt;
font-weight: bold;
cursor: pointer;
}
@mixin dynamic-color-hov($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-dark:HOVER {
background-color: darken($color, 17%) !important;
border-color: darken($color, 20%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
&.dyn-light:HOVER {
background-color: lighten($color, 17%) !important;
border-color: lighten($color, 20%) !important;
}
}
@mixin dynamic-color($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
}
.btn-primary {
background-color: $primary-color;
border-color: darken($primary-color, 17%);
@ -564,18 +360,6 @@ HTML,BODY {
background-color: darken($danger-color, 10%) !important;
}
.nav-pills .nav-link.active, .nav-pills .show>.nav-link {
background-color: $nav-tab-color;
}
.nav-pills A {
color: #424242;
}
.nav-pills I {
margin-right: 10px;
}
@keyframes fadeInOut {
0% { opacity:1; }
50% { opacity:0.3; }
@ -600,181 +384,13 @@ HTML,BODY {
-webkit-animation: fadeInOut 1s infinite;
-moz-animation: fadeInOut 1s infinite;
-o-animation: fadeInOut 1s infinite;
animation: fadeInOut 21 infinite;
}
.CodeMirror {
/* Bootstrap Settings */
box-sizing: border-box;
margin: 0;
font: inherit;
overflow: auto;
display: block;
width: 100%;
padding: 0px;
font-size: 14px;
line-height: 1.5;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
/* Code Mirror Settings */
font-family: monospace;
position: relative;
height:80vh;
}
.CodeMirror-focused {
/* Bootstrap Settings */
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
}
.switch {
font-size: 1rem;
position: relative;
}
.switch input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0;
}
.switch input + label {
position: relative;
min-width: calc(calc(2.375rem * .8) * 2);
border-radius: calc(2.375rem * .8);
height: calc(2.375rem * .8);
line-height: calc(2.375rem * .8);
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem);
}
.switch input + label::before,
.switch input + label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(calc(2.375rem * .8) * 2);
bottom: 0;
display: block;
}
.switch input + label::before {
right: 0;
background-color: #dee2e6;
border-radius: calc(2.375rem * .8);
transition: 0.2s all;
}
.switch input + label::after {
top: 2px;
left: 2px;
width: calc(calc(2.375rem * .8) - calc(2px * 2));
height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%;
background-color: white;
transition: 0.2s all;
}
.switch-rd-gr input:checked + label::before {
background-color: #29b10c !important;
}
.switch input:checked + label::before {
background-color: #08d;
}
.switch input:checked + label::after {
margin-left: calc(2.375rem * .8);
}
.switch input:focus + label::before {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25);
}
.switch input:disabled + label {
color: #868e96;
cursor: not-allowed;
}
.switch input:disabled + label::before {
background-color: #e9ecef;
}
.switch.switch-sm {
font-size: 0.875rem;
}
.switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem);
}
.switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2);
}
.switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2));
}
.switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8);
}
.switch.switch-lg {
font-size: 1.25rem;
}
.switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8);
line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem);
}
.switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2);
}
.switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2));
}
.switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8);
}
.switch + .switch {
margin-left: 1rem;
animation: fadeInOut 1s infinite;
}
.sortable_drag {
background-color: #0000000f;
}
.drag_icon {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
width: 25px;
height: 25px;
display: inline-block;
margin-right: 5px;
margin-left: -10px;
text-align: center;
color: #b1b1b1;
}
/* (Optional) Apply a "closed-hand" cursor during drag operation. */
.drag_icon:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
.switch_btn {
float: right;
margin: -1px 0px 0px 0px;
@ -799,7 +415,7 @@ HTML,BODY {
}
.jumbotron {
background-color: white;
background-color: rgba(0,0,0,0);
}
.toggle-service {
@ -809,19 +425,6 @@ HTML,BODY {
cursor: pointer;
}
.list-group-item {
min-height: 85pt;
}
.list-group-item:HOVER {
background-color: #fff;
}
.index_container {
min-height: 980pt;
background-color: $container-color;
}
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {

View File

@ -0,0 +1,294 @@
@import 'variables';
@import 'mixin';
.copy-btn {
position: absolute;
right: 0;
}
.copy-btn BUTTON {
background-color: white;
margin: 6px;
height: 26px;
font-size: 8pt;
padding: 5px 7px;
border: 1px solid #a7a7a7;
border-radius: 4px !important;
}
.form-control {
background-color: $input-background;
border: $input-border;
color: $input-color;
}
.form-control:FOCUS {
background-color: lighten($input-background, 4%) !important;
border: $input-border;
color: $input-color;
}
.form-control[readonly] {
background-color: lighten($background-color, 12%) !important;
color: darken($background-color, 5%) !important;
}
/* The slider itself */
.slider {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 5px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
}
/* Mouse-over effects */
.slider:hover {
opacity: 1; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
border-radius: 50%;
width: 20px; /* Set a specific slider handle width */
height: 20px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.slider::-moz-range-thumb {
width: 15px; /* Set a specific slider handle width */
height: 15px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.inputTags-field {
border: 0;
background-color: transparent;
padding-top: .13rem;
}
input.inputTags-field:focus {
outline-width: 0;
}
.inputTags-list {
display: block;
width: 100%;
min-height: calc(2.25rem + 2px);
padding: .2rem .35rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.inputTags-item {
background-color: #3aba39;
margin-right: 5px;
padding: 5px 8px;
font-size: 10pt;
color: white;
border-radius: 4px;
}
.inputTags-item .close-item {
margin-left: 6px;
font-size: 13pt;
font-weight: bold;
cursor: pointer;
}
.switch {
font-size: 1rem;
position: relative;
}
.switch input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0;
}
.switch input + label {
position: relative;
min-width: calc(calc(2.375rem * .8) * 2);
border-radius: calc(2.375rem * .8);
height: calc(2.375rem * .8);
line-height: calc(2.375rem * .8);
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem);
}
.switch input + label::before,
.switch input + label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(calc(2.375rem * .8) * 2);
bottom: 0;
display: block;
}
.switch input + label::before {
right: 0;
background-color: #dee2e6;
border-radius: calc(2.375rem * .8);
transition: 0.2s all;
}
.switch input + label::after {
top: 2px;
left: 2px;
width: calc(calc(2.375rem * .8) - calc(2px * 2));
height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%;
background-color: white;
transition: 0.2s all;
}
.switch-rd-gr input:checked + label::before {
background-color: #29b10c !important;
}
.switch input:checked + label::before {
background-color: #08d;
}
.switch input:checked + label::after {
margin-left: calc(2.375rem * .8);
}
.switch input:focus + label::before {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25);
}
.switch input:disabled + label {
color: #868e96;
cursor: not-allowed;
}
.switch input:disabled + label::before {
background-color: #e9ecef;
}
.switch.switch-sm {
font-size: 0.875rem;
}
.switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem);
}
.switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2);
}
.switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2));
}
.switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8);
}
.switch.switch-lg {
font-size: 1.25rem;
}
.switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8);
line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem);
}
.switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2);
}
.switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2));
}
.switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8);
}
.switch + .switch {
margin-left: 1rem;
}
.CodeMirror {
/* Bootstrap Settings */
box-sizing: border-box;
margin: 0;
font: inherit;
overflow: auto;
display: block;
width: 100%;
padding: 0px;
font-size: 14px;
line-height: 1.5;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
/* Code Mirror Settings */
font-family: monospace;
position: relative;
height:80vh;
}
.CodeMirror-focused {
/* Bootstrap Settings */
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
}
.nav-pills .nav-link.active, .nav-pills .show>.nav-link {
background-color: $nav-tab-color;
}
.nav-pills A {
color: $text-color;
}
.nav-pills I {
margin-right: 10px;
}
.drag_icon {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
width: 25px;
height: 25px;
display: inline-block;
margin-right: 5px;
margin-left: -10px;
text-align: center;
color: #b1b1b1;
}
/* (Optional) Apply a "closed-hand" cursor during drag operation. */
.drag_icon:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}

View File

@ -0,0 +1,128 @@
@import 'variables';
@import 'mixin';
HTML,BODY {
background-color: $background-color;
color: $text-color;
}
A {
color: $text-color;
}
A:HOVER {
color: lighten($text-color, 12%) !important;
}
.text-muted {
color: darken($text-color, 30%) !important;
}
.day-success {
background-color: $day-success-background;
}
.day-success:HOVER {
background-color: lighten($day-success-background, 2%) !important;
}
.day-error {
background-color: $day-error-background;
}
.day-error:HOVER {
background-color: lighten($day-error-background, 2%) !important;
}
.contain-card {
.card-header {
font-size: 1.15rem;
}
.dropdown-menu {
background-color: rgba(239, 239, 239, 0.65);
}
}
.navbar {
margin-bottom: 30px;
color: $navbar-color;
background-color: $navbar-background;
}
.table {
color: $text-color;
}
.nav-pills {
border-radius: $global-border-radius;
}
.nav-link {
color: $navbar-color;
}
.form-control {
border-radius: $global-border-radius;
}
.card {
background-color: $card-background;
border: $card-border;
box-shadow: $card-shadow;
}
.card-body {
overflow: hidden;
}
.card-body H4 A {
color: $service-title;
text-decoration: none;
}
.card-title A {
color: $service-title;
text-decoration: none;
}
.card-body .badge {
color: #fff;
}
.list-group-item {
min-height: 85pt;
background-color: $group-list-background;
}
.list-group-item:HOVER {
background-color: lighten($group-list-background, 2%) !important;
}
.list-group-item A {
color: $group-list-title;
}
.container {
padding-top: 20px;
padding-bottom: 25px;
max-width: $max-width;
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, 0.15) !important;
background-color: $container-color;
}
.footer {
text-decoration: none;
margin-top: 20px;
}
.footer A {
color: $footer-text-color;
text-decoration: none;
}
.footer A:HOVER {
color: #6d6d6d;
}

View File

@ -1,2 +1,4 @@
@import 'base';
@import 'layout';
@import 'forms';
@import 'mobile';

View File

@ -0,0 +1,29 @@
@mixin dynamic-color-hov($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-dark:HOVER {
background-color: darken($color, 17%) !important;
border-color: darken($color, 20%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
&.dyn-light:HOVER {
background-color: lighten($color, 17%) !important;
border-color: lighten($color, 20%) !important;
}
}
@mixin dynamic-color($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
}

View File

@ -1,16 +1,28 @@
/* Index Page */
$background-color: #fcfcfc;
$container-color: #fcfcfc;
$background-color: #f5f5f5;
$container-color: #ffffff;
$text-color: #1d1d1d;
$max-width: 860px;
$title-color: #464646;
$description-color: #939393;
$title-color: #4e4e4e;
$description-color: #828282;
$subtitle-color: #747474;
$mobile-card-shadow: 2px 3px 10px #b7b7b7;
$group-list-background: #fafafa;
$group-list-title: #474747;
$navbar-color: #1c1c1c;
$navbar-background: #ffffff;
$input-background: #fdfdfd;
$input-color: #4e4e4e;
$input-border: 1px solid #c9c9c9;
$day-success-background: #18ce08;
$day-error-background: #d50a0a;
/* Status Container */
$service-background: #ffffff;
$service-border: 1px solid rgba(0,0,0,.125);
$service-title: #444444;
$card-background: #fcfcfc;
$card-border: 1px solid rgba(76, 76, 76, 0.12);
$card-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.08);
$service-title: #3e3e3e;
$service-title-size: 1.8rem;
$service-stats-color: #4f4f4f;
$service-description-color: #fff;

View File

@ -1,5 +1,5 @@
<template>
<div class="card text-black-50 bg-white mb-5">
<div class="card mb-5">
<div class="card-header">Cache</div>
<div class="card-body">
<span v-if="!cache" class="text-muted">There are no cached pages yet!</span>

View File

@ -2,40 +2,81 @@
<div class="col-12">
<h2>{{service.name}} Checkins</h2>
<p class="mb-3">Tell your service to send a routine HTTP request to a Statping Checkin.</p>
<div v-for="(checkin, i) in checkins" class="col-12 alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{checkin.name}}</span>
<span class="float-right font-2">Last checkin {{ago(checkin.last_hit)}}</span>
<span class="float-right font-2 mr-3">Check Every {{checkin.interval}} seconds</span>
<span class="float-right font-2 mr-3">Grace Period {{checkin.grace}} seconds</span>
<span class="d-block mt-2">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<span class="small">Send a GET request to this URL every {{checkin.interval}} seconds
<button @click="deleteCheckin(checkin)" type="button" class="btn btn-danger btn-xs float-right mt-1">Delete</button>
<div v-for="(checkin, i) in checkins" class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
{{checkin.name}}
<button @click="deleteCheckin(checkin)" class="btn btn-sm btn-danger float-right text-uppercase">Delete</button>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/checkin/${checkin.api_key}`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
<span class="small">Send a GET request to this URL every {{checkin.interval}} minutes</span>
<span class="small float-right mt-1">Requested {{ago(checkin.last_hit)}} ago</span>
<span class="small float-right mt-1 mr-3">Request expected every {{checkin.interval}} minutes</span>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded = !expanded" :icon="expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{checkin.name}} Records
</div>
<div class="card-body" :class="{'d-none': !expanded}">
<div class="alert alert-primary small" :class="{'alert-success': hit.success, 'alert-danger': !hit.success}" v-for="(hit, i) in records(checkin)">
Checkin {{hit.success ? "Request" : "Failure"}} at {{hit.created_at}}
</div>
</div>
</div>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="curl_expanded = !curl_expanded" :icon="curl_expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
Cronjob Task
</div>
<div class="card-body" :class="{'d-none': !curl_expanded}">
This cronjob script will request the checkin endpoint every {{checkin.interval}} minutes. Add this cronjob task to the machine running this service.
<div class="input-group mt-2">
<input type="text" class="form-control" :value="`${checkin.interval} * * * * /usr/bin/curl ${core.domain}/checkin/${checkin.api_key} >/dev/null 2>&1`" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${checkin.interval} * * * * /usr/bin/curl ${core.domain}/checkin/${checkin.api_key} >/dev/null 2>&1`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
<span class="small d-block">Using CURL</span>
</div>
</div>
</div>
<div class="card-footer">
<span :class="{'text-success': last_record(checkin).success, 'text-danger': !last_record(checkin).success}">
{{last_record(checkin).success ? "Checkin is currently working correctly" : "Checkin is currently failing"}}
</span>
</span>
</div>
</div>
<div class="col-12 alert alert-light">
<div class="card text-black-50 bg-white mt-4">
<div class="card-header text-capitalize">Create Checkin</div>
<div class="card-body">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
<div class="col-3">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-3">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
<button :disabled="btn_disabled" @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
@ -49,11 +90,14 @@ export default {
return {
service: {},
ready: false,
expanded: false,
curl_expanded: false,
checkin: {
name: "",
interval: 60,
grace: 60,
service_id: 0
interval: 1,
service_id: 0,
hits: [],
failures: []
}
}
},
@ -64,6 +108,12 @@ export default {
core() {
return this.$store.getters.core
},
btn_disabled() {
if (this.checkin.name === "" || this.checkin.interval <= 0) {
return true
}
return false
},
},
async created() {
if (this.$route.params) {
@ -74,22 +124,37 @@ export default {
}
},
methods: {
records(checkin) {
let hits = []
let failures = []
checkin.hits.forEach((hit) => {
hits.push({success: true, created_at: this.parseISO(hit.created_at), id: hit.id})
})
checkin.failures.forEach((failure) => {
failures.push({success: false, created_at: this.parseISO(failure.created_at), id: failure.id})
})
return hits.concat(failures).sort((a, b) => {return a.created_at-b.created_at}).reverse().slice(0,32)
},
last_record(checkin) {
const r = this.records(checkin)
return r[0]
},
fixInts() {
const c = this.checkin
this.checkin.interval = parseInt(c.interval)
this.checkin.grace = parseInt(c.grace)
return this.checkin
},
async saveCheckin() {
const c = this.fixInts()
await Api.checkin_create(c)
await this.updateCheckins()
this.checkin.name = ""
await this.load()
},
async deleteCheckin(checkin) {
await Api.checkin_delete(checkin)
await this.updateCheckins()
await this.load()
},
async updateCheckins() {
async load() {
const checkins = await Api.checkins()
this.$store.commit('setCheckins', checkins)
}

View File

@ -1,6 +1,6 @@
<template>
<div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('top_nav.announcements') }}</div>
<div class="card-body pt-0">
<table class="table table-striped">

View File

@ -1,6 +1,6 @@
<template>
<div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('top_nav.services') }}
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-outline-success float-right">
<font-awesome-icon icon="plus"/> Create
@ -11,7 +11,7 @@
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('top_nav.groups') }}</div>
<div class="card-body pt-0">
<table class="table">

View File

@ -1,6 +1,6 @@
<template>
<div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('top_nav.users') }}</div>
<div class="card-body pt-0">
<table class="table table-striped">

View File

@ -35,7 +35,7 @@
</li>
</ul>
<div class="text-center">
<span class="text-black-50">{{total}} Total</span>
<span>{{total}} Failures</span>
</div>
</nav>
</div>
@ -50,7 +50,7 @@ export default {
data() {
return {
service: {},
failures: [],
fails: [],
limit: 10,
offset: 0,
total: 0,
@ -58,6 +58,9 @@ export default {
}
},
computed: {
failures() {
return this.fails.sort(function(a,b) {return b.id - a.id;});
},
pages() {
return Math.floor(this.total / this.limit)
},
@ -91,7 +94,7 @@ export default {
await this.load()
},
async load() {
this.failures = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
this.fails = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<div class="col-12">
<div v-for="incident in incidents" :key="incident.id" class="card contain-card text-black-50 bg-white mb-4">
<div v-for="incident in incidents" :key="incident.id" class="card contain-card mb-4">
<div class="card-header">Incident: {{incident.title}}
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" /> Delete
@ -14,7 +14,7 @@
</div>
<div class="card contain-card text-black-50 bg-white">
<div class="card contain-card">
<div class="card-header">Create Incident</div>
<div class="card-body">
<form @submit.prevent="createIncident">

View File

@ -83,7 +83,7 @@
</router-link>
</div>
<div class="col-12 col-md-3 mb-2 mb-md-0 mt-0 mt-md-1">
<span class="text-black-50 float-md-right">
<span class="float-md-right">
{{$t('uptime', [service.online_7_days])}}
</span>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div class="card text-black-50 bg-white mb-5">
<div class="card mb-5">
<div class="card-header">Theme Editor</div>
<div class="card-body">
<div v-if="error" class="alert alert-danger mt-3" style="white-space: pre-line;">

View File

@ -1,5 +1,5 @@
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<nav class="navbar navbar-expand-lg">
<router-link to="/" class="navbar-brand">Statping</router-link>
<button @click="navopen = !navopen" class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<font-awesome-icon v-if="!navopen" icon="bars"/>

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="d-flex mt-3 mb-2">
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'mini_error': d.amount > 0, 'mini_success': d.amount === 0}">
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'day-error': d.amount > 0, 'day-success': d.amount === 0}">
<span v-if="d.amount != 0" class="small">{{d.amount}}</span>
</div>
</div>

View File

@ -5,7 +5,7 @@
<span class="badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
</div>
<div class="col-md-12 col-12 mt-2 font-3">{{update.message}}</div>
<div class="col-12 font-1 float-right text-black-50 mt-2">{{ago(update.created_at)}} ago</div>
<div class="col-12 font-1 float-right mt-2">{{ago(update.created_at)}} ago</div>
</div>
</div>
</template>

View File

@ -1,7 +1,10 @@
<template>
<div>
<div class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
<div class="card text-black-50 bg-white mt-3 mb-3">
<div class="card-header text-capitalize">Service Latency</div>
<div class="card-body">
<div class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
</div>
</div>
</template>

View File

@ -21,8 +21,8 @@
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>

View File

@ -1,5 +1,5 @@
<template>
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card contain-card mb-3">
<div class="card-header">{{group.id ? `Update ${group.name}` : "Create Group"}}
<transition name="slide-fade">
<button @click="removeEdit" v-if="group.id" class="btn float-right btn-danger btn-sm">

View File

@ -1,5 +1,5 @@
<template>
<div class="card-body bg-light pt-3">
<div class="card-body pt-3">
<div v-if="updates.length===0" class="alert alert-link text-danger">
No updates found, create a new Incident Update below.

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="card contain-card text-black-50 bg-white mb-5">
<div class="card contain-card mb-5">
<div class="card-header">{{message.id ? `Update ${message.title}` : "Create Announcement"}}
<transition name="slide-fade">
<button @click="removeEdit" v-if="message.id" class="btn btn-sm float-right btn-danger btn-sm">

View File

@ -1,7 +1,7 @@
<template>
<div>
<form @submit.prevent="saveNotifier">
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card contain-card mb-3">
<div class="card-header text-capitalize">
{{notifier.title}}
<span @click="enableToggle" class="switch switch-sm switch-rd-gr float-right">
@ -69,7 +69,7 @@
</div>
</div>
<div v-if="notifier.data_type" class="card text-black-50 bg-white mb-3">
<div v-if="notifier.data_type" class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded = !expanded" :icon="expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{notifier.title}} Outgoing Request
@ -103,7 +103,7 @@
</form>
<div v-if="error || success" class="card text-black-50 bg-white mb-3">
<div v-if="error || success" class="card mb-3">
<div class="card-body">
<div v-if="error && !success" class="alert alert-danger col-12" role="alert">
@ -119,7 +119,7 @@
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
@ -128,11 +128,11 @@
</button>
</div>
<div class="col-12 col-md-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="testNotifier('success')" :disabled="loadingTest" class="btn btn-outline-dark btn-block text-capitalize test-notifier">
<button @click.prevent="testNotifier('success')" :disabled="loadingTest" class="btn btn-secondary btn-block text-capitalize test-notifier">
<font-awesome-icon v-if="loadingTest" icon="circle-notch" class="mr-2" spin/>{{loadingTest ? "Loading..." : "Test Success"}}</button>
</div>
<div class="col-12 col-md-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="testNotifier('failure')" :disabled="loadingTest" class="btn btn-outline-dark btn-block text-capitalize test-notifier">
<button @click.prevent="testNotifier('failure')" :disabled="loadingTest" class="btn btn-secondary btn-block text-capitalize test-notifier">
<font-awesome-icon v-if="loadingTest" icon="circle-notch" class="mr-2" spin/>{{loadingTest ? "Loading..." : "Test Failure"}}</button>
</div>
</div>

View File

@ -1,6 +1,6 @@
<template>
<form @submit.prevent="saveOAuth">
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
Internal Login
<span @click="local_enabled = !!local_enabled" class="switch switch-sm switch-rd-gr float-right">
@ -12,7 +12,7 @@
Use Statping's default authentication to allow users you've created to login.
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded.github = !expanded.github" :icon="expanded.github ? 'minus' : 'plus'" class="mr-2 pointer"/>
Github Settings
@ -69,7 +69,7 @@
</div>
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
<font-awesome-icon @click="expanded.google = !expanded.google" :icon="expanded.google ? 'minus' : 'plus'" class="mr-2 pointer"/>
Google Settings
@ -113,7 +113,7 @@
</div>
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
<font-awesome-icon @click="expanded.slack = !expanded.slack" :icon="expanded.slack ? 'minus' : 'plus'" class="mr-2 pointer"/>
Slack Settings
@ -165,7 +165,7 @@
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
<font-awesome-icon @click="expanded.custom = !expanded.custom" :icon="expanded.custom ? 'minus' : 'plus'" class="mr-2 pointer"/>
Custom oAuth Settings

View File

@ -1,6 +1,6 @@
<template>
<form v-if="service.type" @submit.prevent="saveService">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('service.info') }}</div>
<div class="card-body">
<div class="form-group row">
@ -67,7 +67,7 @@
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card contain-card mb-4">
<div class="card-header">Request Details</div>
<div class="card-body">
@ -202,7 +202,7 @@
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card contain-card mb-4">
<div class="card-header">Notification Options</div>
<div class="card-body">

View File

@ -1,5 +1,5 @@
<template>
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card contain-card mb-3">
<div class="card-header"> {{user.id ? `Update ${user.username}` : "Create User"}}
<transition name="slide-fade">
<button @click.prevent="removeEdit" v-if="user.id" class="btn btn-sm float-right btn-danger btn-sm">Close</button>

View File

@ -85,7 +85,6 @@ export default Vue.mixin({
copy(txt) {
this.$copyText(txt).then(function (e) {
alert('Copied: \n'+txt)
console.log(e)
});
},
serviceLink(service) {

View File

@ -1,5 +1,5 @@
<template>
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="container col-md-7 col-sm-12 mt-md-5">
<TopNav :admin="admin"/>
<router-view :admin="admin"/>
</div>

View File

@ -1,9 +1,9 @@
<template>
<div class="col-12 bg-white p-4" v-html="<a class=\"scrollclick\" href=\"#\" data-id=\"page_0\">Types of Monitoring</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_1\">Features</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_2\">Start Statping</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_3\">Linux</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_4\">Mac</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_5\">Windows</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_6\">AWS EC2</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_7\">Docker</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_8\">Mobile App</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_9\">Heroku</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_10\">API</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_11\">Makefile</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_12\">Notifiers</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_13\">Notifier Events</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_14\">Notifier Example</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_15\">Prometheus Exporter</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_16\">SSL</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_17\">Config with .env File</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_18\">Static Export</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_19\">Statping Plugins</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_20\">Statuper</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_21\">Build and Test</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_22\">Contributing</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_23\">PGP Signature</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_24\">Testing</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_25\">Deployment</a><br>\n\n<div class=\"mt-5\" id=\"page_0\"><h1>Types of Monitoring</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_1\"><h1>Features</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_2\"><h1>Start Statping</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_3\"><h1>Linux</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_4\"><h1>Mac</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_5\"><h1>Windows</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_6\"><h1>AWS EC2</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_7\"><h1>Docker</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_8\"><h1>Mobile App</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_9\"><h1>Heroku</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_10\"><h1>API</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_11\"><h1>Makefile</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_12\"><h1>Notifiers</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_13\"><h1>Notifier Events</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_14\"><h1>Notifier Example</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_15\"><h1>Prometheus Exporter</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_16\"><h1>SSL</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_17\"><h1>Config with .env File</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_18\"><h1>Static Export</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_19\"><h1>Statping Plugins</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_20\"><h1>Statuper</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_21\"><h1>Build and Test</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_22\"><h1>Contributing</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_23\"><h1>PGP Signature</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_24\"><h1>Testing</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_25\"><h1>Deployment</h1></div>\n"></div>
<div class="col-12 p-4" v-html="<a class=\"scrollclick\" href=\"#\" data-id=\"page_0\">Types of Monitoring</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_1\">Features</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_2\">Start Statping</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_3\">Linux</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_4\">Mac</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_5\">Windows</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_6\">AWS EC2</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_7\">Docker</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_8\">Mobile App</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_9\">Heroku</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_10\">API</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_11\">Makefile</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_12\">Notifiers</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_13\">Notifier Events</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_14\">Notifier Example</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_15\">Prometheus Exporter</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_16\">SSL</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_17\">Config with .env File</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_18\">Static Export</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_19\">Statping Plugins</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_20\">Statuper</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_21\">Build and Test</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_22\">Contributing</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_23\">PGP Signature</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_24\">Testing</a><br><a class=\"scrollclick\" href=\"#\" data-id=\"page_25\">Deployment</a><br>\n\n<div class=\"mt-5\" id=\"page_0\"><h1>Types of Monitoring</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_1\"><h1>Features</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_2\"><h1>Start Statping</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_3\"><h1>Linux</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_4\"><h1>Mac</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_5\"><h1>Windows</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_6\"><h1>AWS EC2</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_7\"><h1>Docker</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_8\"><h1>Mobile App</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_9\"><h1>Heroku</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_10\"><h1>API</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_11\"><h1>Makefile</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_12\"><h1>Notifiers</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_13\"><h1>Notifier Events</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_14\"><h1>Notifier Example</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_15\"><h1>Prometheus Exporter</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_16\"><h1>SSL</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_17\"><h1>Config with .env File</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_18\"><h1>Static Export</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_19\"><h1>Statping Plugins</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_20\"><h1>Statuper</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_21\"><h1>Build and Test</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_22\"><h1>Contributing</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_23\"><h1>PGP Signature</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_24\"><h1>Testing</h1></div>\n\n\n<div class=\"mt-5\" id=\"page_25\"><h1>Deployment</h1></div>\n"></div>
</template>
<script>
export default {
name: 'Help',
}
</script>
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="container col-md-7 col-sm-12 sm-container index_container">
<div class="container col-md-7 col-sm-12 sm-container">
<Header/>

View File

@ -1,5 +1,5 @@
<template>
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="container col-md-7 col-sm-12 mt-md-5">
<div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2">
<div class="col-12 col-md-8 offset-md-2 mb-4">
<img alt="Statping Login" class="col-12 mt-5 mt-md-0" style="max-width:650px" src="banner.png">

View File

@ -1,5 +1,5 @@
<template>
<div class="col-12 bg-white p-4">
<div class="col-12 p-4">
<p v-if="logs.length === 0" class="text-monospace sm">
Loading Logs...
</p>

View File

@ -1,5 +1,5 @@
<template>
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="container col-md-7 col-sm-12 mt-md-5">
<div class="col-12 mb-4">
@ -18,41 +18,51 @@
<MessageBlock v-for="message in messagesInRange" v-bind:key="message.id" :message="message"/>
<div class="row mt-5 mb-4">
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="onnn" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="btn btn-white text-left" required />
<small class="d-block">From {{this.format(new Date(start_time))}}</small>
</div>
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="onnn" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="btn btn-white text-left" required />
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div>
<div class="col-12 col-md-2">
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
<option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option>
<option value="15m">15 Minute</option>
<option value="30m">30 Minutes</option>
<option value="1h">1 Hour</option>
<option value="3h">3 Hours</option>
<option value="6h">6 Hours</option>
<option value="12h">12 Hours</option>
<option value="24h">1 Day</option>
<option value="168h">7 Days</option>
<option value="360h">15 Days</option>
</select>
<small class="d-block d-md-none d-block">Increment Timeframe</small>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">Timeframe</div>
<div class="card-body">
<div class="row">
<div class="col-12 col-md-4 font-2">
<flatPickr :disabled="loading" @on-change="onnn" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="btn btn-white text-left" required />
<small class="d-block">From {{this.format(new Date(start_time))}}</small>
</div>
<div class="col-12 col-md-4 font-2">
<flatPickr :disabled="loading" @on-change="onnn" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="btn btn-white text-left" required />
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div>
<div class="col-12 col-md-4">
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
<option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option>
<option value="15m">15 Minute</option>
<option value="30m">30 Minutes</option>
<option value="1h">1 Hour</option>
<option value="3h">3 Hours</option>
<option value="6h">6 Hours</option>
<option value="12h">12 Hours</option>
<option value="24h">1 Day</option>
<option value="168h">7 Days</option>
<option value="360h">15 Days</option>
</select>
<small class="d-block d-md-none d-block">Increment Timeframe</small>
</div>
</div>
</div>
</div>
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
<div v-if="!loading" class="col-12">
<div v-if="!loading" class="row">
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
</div>
<div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/>
<div class="card text-black-50 bg-white mb-3">
<div class="card-header text-capitalize">Service Failures</div>
<div class="card-body">
<div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/>
</div>
</div>
</div>
</div>
@ -378,8 +388,7 @@ export default {
this.loading = false
},
async fetchUptime() {
const uptime = await Api.service_uptime(this.id, this.params.start, this.params.end)
window.console.log(uptime)
const uptime = await Api.service_uptime(this.service.id, this.params.start, this.params.end)
this.uptime_data = this.parse_uptime(uptime)
},
parse_uptime(timedata) {

View File

@ -60,14 +60,14 @@
<div class="tab-content" id="v-pills-tabContent">
<div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-home-tab'), show: liClass('v-pills-home-tab')}" id="v-pills-home" role="tabpanel" aria-labelledby="v-pills-home-tab">
<div class="card text-black-50 bg-white">
<div class="card">
<div class="card-header">Statping Settings</div>
<div class="card-body">
<CoreSettings/>
</div>
</div>
<div class="card text-black-50 bg-white mt-3">
<div class="card mt-3">
<div class="card-header">API Settings</div>
<div class="card-body">
<div class="form-group row">
@ -80,7 +80,9 @@
</div>
</div>
<small class="form-text text-muted">API Secret is used for read, create, update and delete routes</small>
<small class="form-text text-muted">You can <a href="#" id="regenkeys" @click="renewApiKeys">Regenerate API Keys</a> if you need to.</small>
<small class="form-text text-muted">You can Regenerate API Keys if you need to.</small>
<button id="regenkeys" @click="renewApiKeys" class="btn btn-sm btn-danger mt-2">Regenerate API Keys</button>
</div>
</div>
</div>

View File

@ -251,6 +251,12 @@ func TestMainApiRoutes(t *testing.T) {
`go_threads`,
},
},
{
Name: "Index Page",
URL: "/",
Method: "GET",
ExpectedStatus: 200,
},
}
for _, v := range tests {

View File

@ -2,6 +2,7 @@ package handlers
import (
"github.com/statping/statping/utils"
"net/url"
"sync"
"time"
)
@ -106,6 +107,13 @@ func (s Storage) Delete(key string) {
func (s Storage) Set(key string, content []byte, duration time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
u, err := url.Parse(key)
if err != nil {
return
}
if u.Query().Get("v") != "" {
return
}
s.items[key] = Item{
Content: content,
Expiration: utils.Now().Add(duration).UnixNano(),

View File

@ -24,8 +24,7 @@ func findCheckin(r *http.Request) (*checkins.Checkin, string, error) {
}
func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
chks := checkins.All()
returnJson(chks, w, r)
returnJson(checkins.All(), w, r)
}
func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
@ -39,8 +38,7 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
func checkinCreateHandler(w http.ResponseWriter, r *http.Request) {
var checkin *checkins.Checkin
err := DecodeJSON(r, &checkin)
if err != nil {
if err := DecodeJSON(r, &checkin); err != nil {
sendErrorJson(err, w, r)
return
}
@ -63,22 +61,27 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
log.Infof("Checking %s was requested", checkin.Name)
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if last := checkin.LastHit(); last == nil {
checkin.Start()
}
hit := &checkins.CheckinHit{
Checkin: checkin.Id,
From: ip,
CreatedAt: utils.Now(),
}
log.Infof("Checking %s was requested", checkin.Name)
err = hit.Create()
if err != nil {
if err := hit.Create(); err != nil {
sendErrorJson(err, w, r)
return
}
checkin.Failing = false
checkin.LastHitTime = utils.Now()
sendJsonAction(hit.Id, "update", w, r)
}

View File

@ -45,8 +45,7 @@ func TestApiCheckinRoutes(t *testing.T) {
Body: `{
"name": "Example Checkin",
"service_id": 1,
"checkin_interval": 300,
"grace_period": 60,
"interval": 300,
"api_key": "example"
}`,
},

View File

@ -26,7 +26,16 @@ func findIncident(r *http.Request) (*incidents.Incident, int64, error) {
func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incids := incidents.FindByService(utils.ToInt(vars["id"]))
id := vars["id"]
if utils.NotNumber(id) {
sendErrorJson(errors.NotNumber, w, r)
return
}
incids := incidents.FindByService(utils.ToInt(id))
if incids == nil {
sendErrorJson(errors.Missing(&incidents.Incident{}, id), w, r)
return
}
returnJson(incids, w, r)
}
@ -54,8 +63,7 @@ func apiCreateIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
update.IncidentId = incid.Id
err = update.Create()
if err != nil {
if err := update.Create(); err != nil {
sendErrorJson(err, w, r)
return
}
@ -74,8 +82,7 @@ func apiCreateIncidentHandler(w http.ResponseWriter, r *http.Request) {
return
}
incident.ServiceId = service.Id
err = incident.Create()
if err != nil {
if err := incident.Create(); err != nil {
sendErrorJson(err, w, r)
return
}
@ -103,8 +110,7 @@ func apiDeleteIncidentHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
err = incident.Delete()
if err != nil {
if err := incident.Delete(); err != nil {
sendErrorJson(err, w, r)
return
}
@ -118,8 +124,7 @@ func apiDeleteIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
err = update.Delete()
if err != nil {
if err := update.Delete(); err != nil {
sendErrorJson(err, w, r)
return
}

View File

@ -3,6 +3,7 @@ package handlers
import (
"github.com/statping/statping/notifiers"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"testing"
)
@ -12,6 +13,8 @@ func TestAttachment(t *testing.T) {
}
func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
slackWebhookUrl := utils.Params.GetString("SLACK_URL")
tests := []HTTPTest{
{
Name: "No Authentication - View All Notifiers",
@ -20,6 +23,13 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "View All Notifiers",
URL: "/api/notifiers",
Method: "GET",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
},
{
Name: "No Authentication - View Notifier",
URL: "/api/notifier/slack",
@ -28,12 +38,76 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update Notifier",
Name: "View Notifier",
URL: "/api/notifier/slack",
Method: "POST",
Method: "GET",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
},
{
Name: "No Authentication - Update Notifier",
URL: "/api/notifier/slack",
Method: "POST",
Body: `{
"method": "slack",
"host": "` + slackWebhookUrl + `",
"enabled": true,
"limits": 55
}`,
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "Update Notifier",
URL: "/api/notifier/slack",
Method: "POST",
Body: `{
"method": "slack",
"host": "` + slackWebhookUrl + `",
"enabled": true,
"limits": 55
}`,
ExpectedStatus: 200,
BeforeTest: SetTestENV,
},
{
Name: "Test Notifier (OnSuccess)",
URL: "/api/notifier/slack/test",
Method: "POST",
Body: `{
"method": "success",
"notifier": {
"enabled": false,
"limits": 60,
"method": "slack",
"host": "` + slackWebhookUrl + `",
"success_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \"The service {{.Service.Name}} is back online.\"\n }\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Service\",\n \"emoji\": true\n },\n \"style\": \"primary\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}",
"failure_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \":warning: The service {{.Service.Name}} is currently offline! :warning:\"\n }\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"section\",\n \"fields\": [{\n \"type\": \"mrkdwn\",\n \"text\": \"*Service:*\\n{{.Service.Name}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*URL:*\\n{{.Service.Domain}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Status Code:*\\n{{.Service.LastStatusCode}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*When:*\\n{{.Failure.CreatedAt}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Downtime:*\\n{{.Service.DowntimeAgo}}\"\n }, {\n \"type\": \"plain_text\",\n \"text\": \"*Error:*\\n{{.Failure.Issue}}\"\n }]\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Offline Service\",\n \"emoji\": true\n },\n \"style\": \"danger\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}"
}
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"success":true`},
BeforeTest: SetTestENV,
},
{
Name: "Test Notifier (OnFailure)",
URL: "/api/notifier/slack/test",
Method: "POST",
Body: `{
"method": "failure",
"notifier": {
"enabled": false,
"limits": 60,
"method": "slack",
"host": "` + slackWebhookUrl + `",
"success_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \"The service {{.Service.Name}} is back online.\"\n }\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Service\",\n \"emoji\": true\n },\n \"style\": \"primary\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}",
"failure_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \":warning: The service {{.Service.Name}} is currently offline! :warning:\"\n }\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"section\",\n \"fields\": [{\n \"type\": \"mrkdwn\",\n \"text\": \"*Service:*\\n{{.Service.Name}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*URL:*\\n{{.Service.Domain}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Status Code:*\\n{{.Service.LastStatusCode}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*When:*\\n{{.Failure.CreatedAt}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Downtime:*\\n{{.Service.DowntimeAgo}}\"\n }, {\n \"type\": \"plain_text\",\n \"text\": \"*Error:*\\n{{.Failure.Issue}}\"\n }]\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Offline Service\",\n \"emoji\": true\n },\n \"style\": \"danger\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}"
}
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"success":true`},
BeforeTest: SetTestENV,
},
}
for _, v := range tests {

45
handlers/oauth_test.go Normal file
View File

@ -0,0 +1,45 @@
package handlers
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestOAuthRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "OAuth Save",
URL: "/api/oauth",
Body: `{
"gh_client_id": "githubid",
"gh_client_secret": "githubsecret",
"google_client_id": "googleid",
"google_client_secret": "googlesecret",
"oauth_domains": "gmail.com,yahoo.com,socialeck.com",
"oauth_providers": "local,slack,google,github",
"slack_client_id": "example.iddd",
"slack_client_secret": "exampleeesecret",
"slack_team": "dev"
}`,
Method: "POST",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
},
{
Name: "OAuth Values",
URL: "/api/oauth",
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{`"slack_client_id":"example.iddd"`},
AfterTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
res, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
t.Log(res)
})
}
}

View File

@ -180,7 +180,6 @@ func Router() *mux.Router {
// API Generic Routes
r.Handle("/metrics", readOnly(promhttp.Handler(), false))
r.Handle("/health", http.HandlerFunc(healthCheckHandler))
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))
r.NotFoundHandler = http.HandlerFunc(error404Handler)
return r
}

View File

@ -67,7 +67,7 @@ func die(err error) {
}
var packageTemplate = template.Must(template.New("").Parse(`<template>
<div class="col-12 bg-white p-4" v-html={{printf "%q" .Compiled}}></div>
<div class="col-12 p-4" v-html={{printf "%q" .Compiled}}></div>
</template>
<script>

View File

@ -16,7 +16,7 @@ import (
var (
log = utils.Log.WithField("type", "source")
TmplBox *rice.Box // HTML and other small files from the 'source/tmpl' directory, this will be loaded into '/assets'
DefaultScss = []string{"scss/main.scss", "scss/base.scss", "scss/mobile.scss"}
DefaultScss = []string{"scss/base.scss", "scss/layout.scss", "scss/main.scss", "scss/mixin.scss", "scss/mobile.scss", "scss/variables.scss"}
)
// Assets will load the Rice boxes containing the CSS, SCSS, JS, and HTML files.

View File

@ -13,8 +13,7 @@ import (
var testCheckin = &Checkin{
ServiceId: 1,
Name: "Test Checkin",
Interval: 60,
GracePeriod: 10,
Interval: 3,
ApiKey: "tHiSiSaTeStXXX",
CreatedAt: utils.Now(),
UpdatedAt: utils.Now(),
@ -39,10 +38,12 @@ func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
SetDB(db)
failures.SetDB(db)
db.AutoMigrate(&Checkin{}, &CheckinHit{}, &failures.Failure{})
db.Create(&testCheckin)
for _, v := range testCheckinHits {
db.Create(&v)
err := db.Create(&v).Error()
require.Nil(t, err)
}
assert.True(t, db.HasTable(&Checkin{}))
assert.True(t, db.HasTable(&CheckinHit{}))

View File

@ -15,6 +15,11 @@ func SetDB(database database.Database) {
}
func (c *Checkin) AfterFind() {
c.AllHits = c.Hits()
c.AllFailures = c.Failures().LastAmount(32)
if last := c.LastHit(); last != nil {
c.LastHitTime = last.CreatedAt
}
metrics.Query("checkin", "find")
}
@ -41,9 +46,6 @@ func (c *Checkin) Create() error {
c.ApiKey = utils.RandomString(32)
}
q := db.Create(c)
c.Start()
go c.checkinRoutine()
return q.Error()
}

View File

@ -2,13 +2,13 @@ package checkins
func (c *Checkin) LastHit() *CheckinHit {
var hit CheckinHit
dbHits.Where("checkin = ?", c.Id).Limit(1).Find(&hit)
dbHits.Where("checkin = ?", c.Id).Last(&hit)
return &hit
}
func (c *Checkin) Hits() []*CheckinHit {
var hits []*CheckinHit
dbHits.Where("checkin = ?", c.Id).Find(&hits)
dbHits.Where("checkin = ?", c.Id).Order("DESC").Find(&hits)
c.AllHits = hits
return hits
}

View File

@ -7,6 +7,7 @@ import (
func (c *Checkin) CreateFailure(f *failures.Failure) error {
f.Checkin = c.Id
c.Failing = true
return failures.DB().Create(f).Error()
}

View File

@ -10,22 +10,11 @@ func (c *Checkin) Expected() time.Duration {
last := c.LastHit()
now := utils.Now()
lastDir := now.Sub(last.CreatedAt)
sub := time.Duration(c.Period() - lastDir)
return sub
return c.Period() - lastDir
}
func (c *Checkin) Period() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%ds", c.Interval))
if duration.Seconds() <= 15 {
return 15 * time.Second
}
return duration
}
// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response)
func (c *Checkin) Grace() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod))
return duration
return time.Duration(c.Interval) * time.Minute
}
// Start will create a channel for the checkin checking go routine
@ -54,12 +43,3 @@ func (c *Checkin) IsRunning() bool {
return true
}
}
// String will return a Checkin API string
func (c *Checkin) String() string {
return c.ApiKey
}
func (c *Checkin) Link() string {
return fmt.Sprintf("%v/checkin/%v", "DOMAINHERE", c.ApiKey)
}

View File

@ -9,27 +9,10 @@ import (
var log = utils.Log.WithField("type", "checkin")
// RecheckCheckinFailure will check if a Service Checkin has been reported yet
func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) {
between := utils.Now().Sub(utils.Now()).Seconds()
if between > float64(c.Interval) {
fmt.Println("rechecking every 15 seconds!")
time.Sleep(15 * time.Second)
guard <- struct{}{}
c.RecheckCheckinFailure(guard)
} else {
fmt.Println("i recovered!!")
}
<-guard
}
// checkinRoutine for checking if the last Checkin was within its interval
func (c *Checkin) checkinRoutine() {
lastHit := c.LastHit()
if lastHit == nil {
return
}
reCheck := c.Period()
CheckinLoop:
for {
select {
@ -38,20 +21,25 @@ CheckinLoop:
c.Failing = false
break CheckinLoop
case <-time.After(reCheck):
log.Infoln(fmt.Sprintf("Checkin '%s' expects a request every %v", c.Name, utils.FormatDuration(c.Period())))
if c.Expected() <= 0 {
issue := fmt.Sprintf("Checkin '%s' is failing, no request since %v", c.Name, lastHit.CreatedAt)
//log.Errorln(issue)
lastHit := c.LastHit()
ago := utils.Now().Sub(lastHit.CreatedAt)
log.Infoln(fmt.Sprintf("Checkin '%s' expects a request every %s last request was %s ago", c.Name, c.Period(), utils.DurationReadable(ago)))
if ago.Seconds() > c.Period().Seconds() {
issue := fmt.Sprintf("Checkin expects a request every %d seconds", c.Interval)
log.Warnln(issue)
fail := &failures.Failure{
Issue: issue,
Method: "checkin",
Service: c.ServiceId,
Checkin: c.Id,
PingTime: c.Expected().Milliseconds(),
PingTime: ago.Milliseconds(),
}
c.CreateFailure(fail)
if err := c.CreateFailure(fail); err != nil {
log.Errorln(err)
}
}
reCheck = c.Period()
}

View File

@ -8,22 +8,20 @@ import (
func Samples() error {
log.Infoln("Inserting Sample Checkins...")
checkin1 := &Checkin{
Name: "Demo Checkin 1",
ServiceId: 1,
Interval: 300,
GracePeriod: 300,
ApiKey: "demoCheckin123",
Name: "Demo Checkin 1",
ServiceId: 1,
Interval: 3,
ApiKey: "demoCheckin123",
}
if err := checkin1.Create(); err != nil {
return err
}
checkin2 := &Checkin{
Name: "Example Checkin 2",
ServiceId: 2,
Interval: 900,
GracePeriod: 300,
ApiKey: utils.RandomString(7),
Name: "Example Checkin 2",
ServiceId: 2,
Interval: 1,
ApiKey: utils.RandomString(7),
}
if err := checkin2.Create(); err != nil {
return err

View File

@ -11,7 +11,6 @@ type Checkin struct {
ServiceId int64 `gorm:"index;column:service" json:"service_id"`
Name string `gorm:"column:name" json:"name"`
Interval int64 `gorm:"column:check_interval" json:"interval"`
GracePeriod int64 `gorm:"column:grace_period" json:"grace"`
ApiKey string `gorm:"column:api_key" json:"api_key"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`

View File

@ -38,7 +38,7 @@ func (f Failurer) List() []*Failure {
func (f Failurer) LastAmount(amount int) []*Failure {
var fail []*Failure
f.db.Order("id asc").Limit(amount).Find(&fail)
f.db.Order("id DESC").Limit(amount).Find(&fail)
return fail
}

View File

@ -79,16 +79,13 @@ func All() []*Incident {
}
func (i *Incident) Create() error {
q := db.Create(i)
return q.Error()
return db.Create(i).Error()
}
func (i *Incident) Update() error {
q := db.Update(i)
return q.Error()
return db.Update(i).Error()
}
func (i *Incident) Delete() error {
q := db.Delete(i)
return q.Error()
return db.Delete(i).Error()
}

View File

@ -32,8 +32,10 @@ func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.AutoMigrate(&Incident{}, &IncidentUpdate{})
db.Create(&example)
SetDB(db)
db.Create(&example)
db.Create(&update1)
db.Create(&update2)
}
func TestFind(t *testing.T) {

View File

@ -7,7 +7,9 @@ import (
// CheckinProcess runs the checkin routine for each checkin attached to service
func CheckinProcess(s *Service) {
for _, c := range s.Checkins() {
c.Start()
if last := c.LastHit(); last != nil {
c.Start()
}
}
}

View File

@ -253,10 +253,6 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
return allServices, nil
}
for _, s := range all() {
if start {
CheckinProcess(s)
}
s.Failures = s.AllFailures().LastAmount(limitedFailures)
for _, c := range s.Checkins() {
s.AllCheckins = append(s.AllCheckins, c)
@ -264,6 +260,9 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
// collect initial service stats
s.UpdateStats()
allServices[s.Id] = s
if start {
CheckinProcess(s)
}
}
return allServices, nil
}

View File

@ -61,11 +61,10 @@ var hit3 = &hits.Hit{
}
var exmapleCheckin = &checkins.Checkin{
ServiceId: 1,
Name: "Example Checkin",
Interval: 60,
GracePeriod: 30,
ApiKey: "wdededede",
ServiceId: 1,
Name: "Example Checkin",
Interval: 3,
ApiKey: "wdededede",
}
var fail1 = &failures.Failure{
@ -504,6 +503,62 @@ func TestServices(t *testing.T) {
assert.Len(t, all, 1)
})
t.Run("Test Load services.yml", func(t *testing.T) {
file := `x-tcpservice: &tcpservice
type: tcp
check_interval: 60
timeout: 15
allow_notifications: true
notify_after: 0
notify_all_changes: true
public: true
redirect: true
x-httpservice: &httpservice
type: http
method: GET
check_interval: 45
timeout: 10
expected_status: 200
allow_notifications: true
notify_after: 2
notify_all_changes: true
public: true
redirect: true
services:
- name: Statping Demo
domain: https://demo.statping.com
<<: *httpservice
- name: Portainer
domain: portainer
port: 9000
<<: *tcpservice
- name: Statping Github
domain: https://github.com/statping/statping
<<: *httpservice`
err := utils.SaveFile(utils.Directory+"/services.yml", []byte(file))
require.Nil(t, err)
assert.FileExists(t, utils.Directory+"/services.yml")
srvs, err := LoadServicesYaml()
require.Nil(t, err)
require.Equal(t, 3, len(srvs.Services))
assert.Equal(t, "Statping Demo", srvs.Services[0].Name)
assert.Equal(t, 45, srvs.Services[0].Interval)
assert.Equal(t, "https://demo.statping.com", srvs.Services[0].Domain)
err = utils.DeleteFile(utils.Directory + "/services.yml")
require.Nil(t, err)
})
t.Run("Test Close", func(t *testing.T) {
assert.Nil(t, db.Close())
})

View File

@ -22,7 +22,3 @@ func (d Duration) Human() string {
func FormatDuration(d time.Duration) string {
return durafmt.ParseShort(d).LimitFirstN(3).String()
}
func rev(f float64) float64 {
return f * -1
}

View File

@ -10,39 +10,25 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"github.com/ararog/timeago"
)
var (
// Directory returns the current path or the STATPING_DIR environment variable
Directory string
disableLogs bool
Directory string
)
type env struct {
data interface{}
}
func NotNumber(val string) bool {
_, err := strconv.ParseInt(val, 10, 64)
return err != nil
}
func (e *env) Duration() time.Duration {
t, err := time.ParseDuration(e.data.(string))
if err != nil {
Log.Errorln(err)
}
return t
}
// ToInt converts a int to a string
func ToInt(s interface{}) int64 {
switch v := s.(type) {
@ -91,17 +77,6 @@ func ToString(s interface{}) string {
}
}
type Timestamp time.Time
type Timestamper interface {
Ago() string
}
// Ago returns a human readable timestamp based on the Timestamp (time.Time) interface
func (t Timestamp) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now(), time.Time(t))
return got
}
// Command will run a terminal command with 'sh -c COMMAND' and return stdout and errOut as strings
// in, out, err := Command("sass assets/scss assets/css/base.css")
func Command(name string, args ...string) (string, string, error) {
@ -187,14 +162,14 @@ func DurationReadable(d time.Duration) string {
// // body - The body or form data to send with HTTP request
// // timeout - Specific duration to timeout on. time.Duration(30 * time.Seconds)
// // You can use a HTTP Proxy if you HTTP_PROXY environment variable
func HttpRequest(url, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration, verifySSL bool, customTLS *tls.Config) ([]byte, *http.Response, error) {
func HttpRequest(endpoint, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration, verifySSL bool, customTLS *tls.Config) ([]byte, *http.Response, error) {
var err error
var req *http.Request
if method == "" {
method = "GET"
}
t1 := Now()
if req, err = http.NewRequest(method, url, body); err != nil {
if req, err = http.NewRequest(method, endpoint, body); err != nil {
return nil, nil, err
}
req.Header.Set("User-Agent", "Statping")
@ -243,6 +218,13 @@ func HttpRequest(url, method string, content interface{}, headers []string, body
return dialer.DialContext(ctx, network, addr)
},
}
if Params.IsSet("HTTP_PROXY") {
proxyUrl, err := url.Parse(Params.GetString("HTTP_PROXY"))
if err != nil {
return nil, nil, err
}
transport.Proxy = http.ProxyURL(proxyUrl)
}
if customTLS != nil {
transport.TLSClientConfig.RootCAs = customTLS.RootCAs
transport.TLSClientConfig.Certificates = customTLS.Certificates
@ -269,8 +251,8 @@ func HttpRequest(url, method string, content interface{}, headers []string, body
}
// record HTTP metrics
metrics.Histo("bytes", float64(len(contents)), url, method)
metrics.Histo("duration", Now().Sub(t1).Seconds(), url, method)
metrics.Histo("bytes", float64(len(contents)), endpoint, method)
metrics.Histo("duration", Now().Sub(t1).Seconds(), endpoint, method)
return contents, resp, err
}

View File

@ -123,13 +123,28 @@ func ExampleStringInt() {
// Output: 42
}
func TestTimestamp_Ago(t *testing.T) {
now := Timestamp(time.Now())
assert.Equal(t, "Just now", now.Ago())
func TestHashPassword(t *testing.T) {
pass := HashPassword("password123")
assert.Equal(t, 60, len(pass))
assert.True(t, CheckHash("password123", pass))
assert.False(t, CheckHash("wrongpasswd", pass))
}
func TestHashPassword(t *testing.T) {
assert.Equal(t, 60, len(HashPassword("password123")))
func TestHuman(t *testing.T) {
assert.Equal(t, "10 seconds", Duration{10 * time.Second}.Human())
assert.Equal(t, "1 day 12 hours", Duration{36 * time.Hour}.Human())
assert.Equal(t, "45 minutes", Duration{45 * time.Minute}.Human())
}
func TestSha256Hash(t *testing.T) {
assert.Equal(t, "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f", Sha256Hash("password123"))
}
func TestNotNumbber(t *testing.T) {
assert.True(t, NotNumber("notint"))
assert.True(t, NotNumber("1293notanint922"))
assert.False(t, NotNumber("0"))
assert.False(t, NotNumber("5"))
}
func TestNewSHA1Hash(t *testing.T) {
@ -190,3 +205,12 @@ func TestConfigLoad(t *testing.T) {
assert.True(t, b("SAMPLE_DATA"))
assert.True(t, b("ALLOW_REPORTS"))
}
func TestPerlin(t *testing.T) {
p := NewPerlin(2, 2, 5, Now().UnixNano())
require.NotNil(t, p)
for hi := 1.; hi <= 100.; hi++ {
assert.NotZero(t, p.Noise1D(hi/500))
}
}

View File

@ -1 +1 @@
0.90.60
0.90.61