pull/429/head
hunterlong 2020-02-19 21:28:39 -08:00
parent b8dbad85fe
commit 7762ca40d7
61 changed files with 2608 additions and 1225 deletions

View File

@ -280,7 +280,6 @@ func recordFailure(s *Service, issue string) {
s.CreateFailure(fail) s.CreateFailure(fail)
s.Online = false s.Online = false
s.SuccessNotified = false s.SuccessNotified = false
s.UpdateNotify = CoreApp.UpdateNotify.Bool
s.DownText = s.DowntimeText() s.DownText = s.DowntimeText()
notifier.OnFailure(s.Service, fail) notifier.OnFailure(s.Service, fail)
} }

View File

@ -40,7 +40,7 @@ func OnFailure(s *types.Service, f *types.Failure) {
} }
// check if User wants to receive every Status Change // check if User wants to receive every Status Change
if s.UpdateNotify { if s.UpdateNotify.Bool {
// send only if User hasn't been already notified about the Downtime // send only if User hasn't been already notified about the Downtime
if !s.UserNotified { if !s.UserNotified {
s.UserNotified = true s.UserNotified = true
@ -69,7 +69,7 @@ func OnSuccess(s *types.Service) {
} }
// check if User wants to receive every Status Change // check if User wants to receive every Status Change
if s.UpdateNotify && s.UserNotified { if s.UpdateNotify.Bool && s.UserNotified {
s.UserNotified = false s.UserNotified = false
} }

View File

@ -10,12 +10,14 @@ const environment = require('./dev.env');
const webpackConfig = merge(commonConfig, { const webpackConfig = merge(commonConfig, {
mode: 'development', mode: 'development',
devtool: 'cheap-module-eval-source-map', devtool: 'inline-cheap-module-source-map',
output: { output: {
path: helpers.root('dist'), path: helpers.root('dist'),
publicPath: '/', publicPath: '/',
filename: 'js/[name].bundle.js', filename: 'js/[name].bundle.js',
chunkFilename: 'js/[name].chunk.js' chunkFilename: 'js/[name].chunk.js',
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]'
}, },
optimization: { optimization: {
runtimeChunk: 'single', runtimeChunk: 'single',

View File

@ -6,7 +6,8 @@
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "rm -rf dist && cross-env NODE_ENV=production webpack --mode production", "build": "rm -rf dist && cross-env NODE_ENV=production webpack --mode production",
"dev": "cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8888 --progress", "dev": "cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8888 --progress",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint",
"test": "cross-env NODE_ENV=development mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3", "@fortawesome/fontawesome-free-solid": "^5.1.0-3",
@ -18,7 +19,7 @@
"axios": "^0.19.1", "axios": "^0.19.1",
"codemirror-colorpicker": "^1.9.66", "codemirror-colorpicker": "^1.9.66",
"core-js": "^3.4.4", "core-js": "^3.4.4",
"moment": "^2.24.0", "date-fns": "^2.9.0",
"querystring": "^0.2.0", "querystring": "^0.2.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-apexcharts": "^1.5.2", "vue-apexcharts": "^1.5.2",
@ -40,6 +41,7 @@
"@babel/preset-env": "~7.8.3", "@babel/preset-env": "~7.8.3",
"@vue/babel-preset-app": "^4.1.2", "@vue/babel-preset-app": "^4.1.2",
"@vue/cli-plugin-babel": "^4.1.0", "@vue/cli-plugin-babel": "^4.1.0",
"@vue/test-utils": "^1.0.0-beta.31",
"babel-eslint": "~10.0", "babel-eslint": "~10.0",
"babel-loader": "~8.0", "babel-loader": "~8.0",
"compression-webpack-plugin": "~2.0", "compression-webpack-plugin": "~2.0",
@ -55,10 +57,15 @@
"eslint-plugin-promise": "~3.5", "eslint-plugin-promise": "~3.5",
"eslint-plugin-standard": "~3.0", "eslint-plugin-standard": "~3.0",
"eslint-plugin-vue": "~5.1", "eslint-plugin-vue": "~5.1",
"expect": "^25.1.0",
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"friendly-errors-webpack-plugin": "~1.7", "friendly-errors-webpack-plugin": "~1.7",
"html-webpack-plugin": "^4.0.0-beta.11", "html-webpack-plugin": "^4.0.0-beta.11",
"jsdom": "^16.2.0",
"jsdom-global": "^3.0.2",
"mini-css-extract-plugin": "~0.5", "mini-css-extract-plugin": "~0.5",
"mocha": "^7.0.1",
"mochapack": "^1.1.13",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "~5.0", "optimize-css-assets-webpack-plugin": "~5.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",

View File

@ -10,11 +10,20 @@
<base href="{{BasePath}}"> <base href="{{BasePath}}">
{{if USE_CDN}} {{if USE_CDN}}
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico">
<link rel="stylesheet" href="https://assets.statping.com/css/bootstrap.min.css">
<link rel="stylesheet" href="https://assets.statping.com/css/base.css">
<link rel="stylesheet" href="https://assets.statping.com/font/all.css">
{{else}} {{else}}
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
{{if USING_ASSETS}}
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="font/all.css">
{{else}}
<% _.each(htmlWebpackPlugin.tags.headTags, function(headTag) { %> <% _.each(htmlWebpackPlugin.tags.headTags, function(headTag) { %>
<%= headTag %> <% }) %> <%= headTag %> <% }) %>
{{end}} {{end}}
{{end}}
</head> </head>
<body> <body>
<noscript> <noscript>

226
frontend/src/API.js Normal file
View File

@ -0,0 +1,226 @@
import axios from 'axios'
const qs = require('querystring')
const tokenKey = "statping_user";
class Api {
constructor() {
}
async core() {
return axios.get('/api').then(response => (response.data))
}
async core_save(obj) {
return axios.post('/api/core', obj).then(response => (response.data))
}
async setup_save(data) {
return axios.post('/api/setup', qs.stringify(data)).then(response => (response.data))
}
async services() {
return axios.get('/api/services').then(response => (response.data))
}
async service(id) {
return axios.get('/api/services/' + id).then(response => (response.data))
}
async service_create(data) {
return axios.post('/api/services', data).then(response => (response.data))
}
async service_update(data) {
return axios.post('/api/services/' + data.id, data).then(response => (response.data))
}
async service_hits(id, start, end, group) {
return axios.get('/api/services/' + id + '/data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
}
async service_heatmap(id, start, end, group) {
return axios.get('/api/services/' + id + '/heatmap').then(response => (response.data))
}
async service_failures(id, start, end, limit = 999, offset = 0) {
return axios.get('/api/services/' + id + '/failures?start=' + start + '&end=' + end + '&limit=' + limit).then(response => (response.data))
}
async service_delete(id) {
return axios.delete('/api/services/' + id).then(response => (response.data))
}
async services_reorder(data) {
return axios.post('/api/reorder/services', data).then(response => (response.data))
}
async groups() {
return axios.get('/api/groups').then(response => (response.data))
}
async groups_reorder(data) {
return axios.post('/api/reorder/groups', data).then(response => (response.data))
}
async group_delete(id) {
return axios.delete('/api/groups/' + id).then(response => (response.data))
}
async group_create(data) {
return axios.post('/api/groups', data).then(response => (response.data))
}
async group_update(data) {
return axios.post('/api/groups/' + data.id, data).then(response => (response.data))
}
async users() {
return axios.get('/api/users').then(response => (response.data))
}
async user_create(data) {
return axios.post('/api/users', data).then(response => (response.data))
}
async user_update(data) {
return axios.post('/api/users/' + data.id, data).then(response => (response.data))
}
async user_delete(id) {
return axios.delete('/api/users/' + id).then(response => (response.data))
}
async messages() {
return axios.get('/api/messages').then(response => (response.data))
}
async message_create(data) {
return axios.post('/api/messages', data).then(response => (response.data))
}
async message_update(data) {
return axios.post('/api/messages/' + data.id, data).then(response => (response.data))
}
async message_delete(id) {
return axios.delete('/api/messages/' + id).then(response => (response.data))
}
async group(id) {
return axios.get('/api/groups/' + id).then(response => (response.data))
}
async notifiers() {
return axios.get('/api/notifiers').then(response => (response.data))
}
async notifier_save(data) {
return axios.post('/api/notifier/' + data.method, data).then(response => (response.data))
}
async notifier_test(data) {
return axios.post('/api/notifier/' + data.method + '/test', data).then(response => (response.data))
}
async integrations() {
return axios.get('/api/integrations').then(response => (response.data))
}
async integration(name) {
return axios.get('/api/integrations/' + name).then(response => (response.data))
}
async integration_save(data) {
return axios.post('/api/integrations/' + data.name, data).then(response => (response.data))
}
async renewApiKeys() {
return axios.get('/api/renew').then(response => (response.data))
}
async cache() {
return axios.get('/api/cache').then(response => (response.data))
}
async clearCache() {
return axios.get('/api/clear_cache').then(response => (response.data))
}
async logs() {
return axios.get('/api/logs').then(response => (response.data))
}
async logs_last() {
return axios.get('/api/logs/last').then(response => (response.data))
}
async theme() {
return axios.get('/api/theme').then(response => (response.data))
}
async theme_generate(create = true) {
if (create) {
return axios.get('/api/theme/create').then(response => (response.data))
} else {
return axios.delete('/api/theme').then(response => (response.data))
}
}
async theme_save(data) {
return axios.post('/api/theme', data).then(response => (response.data))
}
async login(username, password) {
const f = {username: username, password: password}
return axios.post('/api/login', qs.stringify(f))
.then(response => (response.data))
}
async logout() {
await axios.get('/api/logout').then(response => (response.data))
return localStorage.removeItem(tokenKey)
}
saveToken(username, token) {
const user = {username: username, token: token}
localStorage.setItem(tokenKey, JSON.stringify(user));
return user
}
async scss_base() {
return await axios({
url: '/scss/base.scss',
method: 'GET',
responseType: 'blob'
}).then((response) => {
const reader = new window.FileReader();
return reader.readAsText(response.data)
})
}
token() {
const tk = localStorage.getItem(tokenKey)
if (!tk) {
return {};
}
return JSON.parse(tk);
}
authToken() {
let user = JSON.parse(localStorage.getItem(tokenKey));
if (user && user.token) {
return {'Authorization': 'Bearer ' + user.token};
} else {
return {};
}
}
async allActions(...all) {
await axios.all([all])
}
}
const api = new Api()
export default api

View File

@ -6,7 +6,7 @@
</template> </template>
<script> <script>
import Api from './components/API'; import Api from './API';
import Footer from "./components/Index/Footer"; import Footer from "./components/Index/Footer";
export default { export default {

View File

@ -1,226 +0,0 @@
import axios from 'axios'
const qs = require('querystring')
const tokenKey = "statping_user";
class Api {
constructor() {
}
async core () {
return axios.get('/api').then(response => (response.data))
}
async core_save (obj) {
return axios.post('/api/core', obj).then(response => (response.data))
}
async setup_save (data) {
return axios.post('/api/setup', qs.stringify(data)).then(response => (response.data))
}
async services () {
return axios.get('/api/services').then(response => (response.data))
}
async service (id) {
return axios.get('/api/services/'+id).then(response => (response.data))
}
async service_create (data) {
return axios.post('/api/services', data).then(response => (response.data))
}
async service_update (data) {
return axios.post('/api/services/'+data.id, data).then(response => (response.data))
}
async service_hits (id, start, end, group) {
return axios.get('/api/services/'+id+'/data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
}
async service_heatmap (id, start, end, group) {
return axios.get('/api/services/'+id+'/heatmap?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
}
async service_failures (id, start, end, limit=999, offset=0) {
return axios.get('/api/services/'+id+'/failures?start=' + start + '&end=' + end + '&limit=' + limit).then(response => (response.data))
}
async service_delete (id) {
return axios.delete('/api/services/'+id).then(response => (response.data))
}
async services_reorder (data) {
return axios.post('/api/reorder/services', data).then(response => (response.data))
}
async groups () {
return axios.get('/api/groups').then(response => (response.data))
}
async groups_reorder (data) {
return axios.post('/api/reorder/groups', data).then(response => (response.data))
}
async group_delete (id) {
return axios.delete('/api/groups/'+id).then(response => (response.data))
}
async group_create (data) {
return axios.post('/api/groups', data).then(response => (response.data))
}
async group_update (data) {
return axios.post('/api/groups/'+data.id, data).then(response => (response.data))
}
async users () {
return axios.get('/api/users').then(response => (response.data))
}
async user_create (data) {
return axios.post('/api/users', data).then(response => (response.data))
}
async user_update (data) {
return axios.post('/api/users/'+data.id, data).then(response => (response.data))
}
async user_delete (id) {
return axios.delete('/api/users/'+id).then(response => (response.data))
}
async messages () {
return axios.get('/api/messages').then(response => (response.data))
}
async message_create (data) {
return axios.post('/api/messages', data).then(response => (response.data))
}
async message_update (data) {
return axios.post('/api/messages/'+data.id, data).then(response => (response.data))
}
async message_delete (id) {
return axios.delete('/api/messages/'+id).then(response => (response.data))
}
async group (id) {
return axios.get('/api/groups/'+id).then(response => (response.data))
}
async notifiers () {
return axios.get('/api/notifiers').then(response => (response.data))
}
async notifier_save (data) {
return axios.post('/api/notifier/'+data.method, data).then(response => (response.data))
}
async notifier_test (data) {
return axios.post('/api/notifier/'+data.method+'/test', data).then(response => (response.data))
}
async integrations () {
return axios.get('/api/integrations').then(response => (response.data))
}
async integration (name) {
return axios.get('/api/integrations/'+name).then(response => (response.data))
}
async integration_save (data) {
return axios.post('/api/integrations/'+data.name, data).then(response => (response.data))
}
async renewApiKeys () {
return axios.get('/api/renew').then(response => (response.data))
}
async cache () {
return axios.get('/api/cache').then(response => (response.data))
}
async clearCache () {
return axios.get('/api/clear_cache').then(response => (response.data))
}
async logs () {
return axios.get('/api/logs').then(response => (response.data))
}
async logs_last () {
return axios.get('/api/logs/last').then(response => (response.data))
}
async theme () {
return axios.get('/api/theme').then(response => (response.data))
}
async theme_generate (create=true) {
if (create) {
return axios.get('/api/theme/create').then(response => (response.data))
} else {
return axios.delete('/api/theme').then(response => (response.data))
}
}
async theme_save (data) {
return axios.post('/api/theme', data).then(response => (response.data))
}
async login (username, password) {
const f = {username: username, password: password}
return axios.post('/api/login', qs.stringify(f))
.then(response => (response.data))
}
async logout () {
await axios.get('/api/logout').then(response => (response.data))
return localStorage.removeItem(tokenKey)
}
saveToken (username, token) {
const user = {username: username, token: token}
localStorage.setItem(tokenKey, JSON.stringify(user));
return user
}
async scss_base () {
return await axios({
url: '/scss/base.scss',
method: 'GET',
responseType: 'blob'
}).then((response) => {
const reader = new window.FileReader();
return reader.readAsText(response.data)
})
}
token () {
const tk = localStorage.getItem(tokenKey)
if (!tk) {
return {};
}
return JSON.parse(tk);
}
authToken () {
let user = JSON.parse(localStorage.getItem(tokenKey));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}
async allActions (...all) {
await axios.all([all])
}
}
const api = new Api()
export default api

View File

@ -0,0 +1,56 @@
<template>
<div>
<h3>Cache</h3>
<div v-if="!cache && cache.length !== 0" class="alert alert-danger">
There are no cached files
</div>
<table class="table">
<thead>
<tr>
<th scope="col">URL</th>
<th scope="col">Size</th>
<th scope="col">Expiration</th>
</tr>
</thead>
<tbody>
<tr v-for="(cache, index) in cache">
<td>{{cache.url}}</td>
<td>{{cache.size}}</td>
<td>{{expireTime(cache.expiration)}}</td>
</tr>
</tbody>
</table>
<button @click.prevent="clearCache" class="btn btn-danger btn-block">Clear Cache</button>
</div>
</template>
<script>
import Api from "../../API";
export default {
name: 'Cache',
data() {
return {
cache: [],
}
},
async mounted() {
this.cache = await Api.cache()
},
methods: {
expireTime(ex) {
return this.toLocal(ex)
},
async clearCache() {
await Api.clearCache()
this.cache = []
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -23,33 +23,24 @@
</template> </template>
<script> <script>
import ServiceInfo from "./ServiceInfo"; import ServiceInfo from "../Service/ServiceInfo";
export default { export default {
name: 'DashboardIndex', name: 'DashboardIndex',
components: { components: {
ServiceInfo ServiceInfo
},
data () {
return {
}
},
computed: {
},
async created() {
}, },
methods: { methods: {
failuresLast24Hours() { failuresLast24Hours() {
let total = 0; let total = 0;
this.$store.getters.services.map((s) => { total += s.failures_24_hours }) this.$store.getters.services.map((s) => {
total += s.failures_24_hours
})
return total return total
}, },
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -37,7 +37,7 @@
</template> </template>
<script> <script>
import Api from "../API" import Api from "../../API"
import FormMessage from "../../forms/Message"; import FormMessage from "../../forms/Message";
export default { export default {

View File

@ -99,7 +99,7 @@
<script> <script>
import FormGroup from "../../forms/Group"; import FormGroup from "../../forms/Group";
import Api from "../../components/API"; import Api from "../../API";
import ToggleSwitch from "../../forms/ToggleSwitch"; import ToggleSwitch from "../../forms/ToggleSwitch";
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
@ -110,7 +110,7 @@
FormGroup, FormGroup,
draggable draggable
}, },
data () { data() {
return { return {
edit: false, edit: false,
group: {} group: {}
@ -124,7 +124,7 @@
async set(value) { async set(value) {
let data = []; let data = [];
value.forEach((s, k) => { value.forEach((s, k) => {
data.push({service: s.id, order: k+1}) data.push({service: s.id, order: k + 1})
}); });
await Api.services_reorder(data) await Api.services_reorder(data)
const services = await Api.services() const services = await Api.services()
@ -138,7 +138,7 @@
async set(value) { async set(value) {
let data = []; let data = [];
value.forEach((s, k) => { value.forEach((s, k) => {
data.push({group: s.id, order: k+1}) data.push({group: s.id, order: k + 1})
}); });
await Api.groups_reorder(data) await Api.groups_reorder(data)
const groups = await Api.groups() const groups = await Api.groups()
@ -189,7 +189,7 @@
} }
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -32,7 +32,7 @@
</template> </template>
<script> <script>
import Api from "../API" import Api from "../../API"
import FormUser from "../../forms/User"; import FormUser from "../../forms/User";
export default { export default {

View File

@ -10,7 +10,7 @@
<script> <script>
import FormGroup from "../../forms/Group"; import FormGroup from "../../forms/Group";
import Api from "../../components/API"; import Api from "../../API";
import ToggleSwitch from "../../forms/ToggleSwitch"; import ToggleSwitch from "../../forms/ToggleSwitch";
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import FormService from "../../forms/Service"; import FormService from "../../forms/Service";

View File

@ -44,7 +44,7 @@
</template> </template>
<script> <script>
import Api from "../API"; import Api from "../../API";
// require component // require component
import { codemirror } from 'vue-codemirror' import { codemirror } from 'vue-codemirror'
@ -136,7 +136,6 @@
this.error = null this.error = null
} }
this.pending = false this.pending = false
window.console.log(resp)
await this.fetchTheme() await this.fetchTheme()
}, },
changeTab (v) { changeTab (v) {

View File

@ -41,7 +41,7 @@
</template> </template>
<script> <script>
import Api from "../API" import Api from "../../API"
export default { export default {
name: 'TopNav', name: 'TopNav',

View File

@ -8,20 +8,7 @@
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span> <span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
</h4> </h4>
<div class="row stats_area mt-5"> <ServiceTopStats :service="service"/>
<div class="col-4">
<span class="lg_number">{{service.avg_response}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{service.online_24_hours}}%</span>
Uptime last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{service.online_7_days}}%</span>
Uptime last 7 Days
</div>
</div>
</div> </div>
</div> </div>
@ -48,10 +35,11 @@
<script> <script>
import ServiceChart from "./ServiceChart"; import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
export default { export default {
name: 'ServiceBlock', name: 'ServiceBlock',
components: {ServiceChart}, components: {ServiceTopStats, ServiceChart},
props: { props: {
service: { service: {
type: Object, type: Object,
@ -61,14 +49,10 @@ export default {
methods: { methods: {
smallText(s) { smallText(s) {
if (s.online) { if (s.online) {
return `Online, last checked ${this.ago(s.last_success)}` return `Online, last checked ${this.ago(this.parseTime(s.last_success))}`
} else { } else {
return `Offline, last error: ${s.last_failure.issue} ${this.ago(s.last_failure.created_at)}` return `Offline, last error: ${s.last_failure.issue} ${this.ago(this.parseTime(s.last_failure.created_at))}`
} }
},
ago(t1) {
const tm = this.parseTime(t1)
return this.duration(this.$moment().utc(), tm)
} }
} }
} }

View File

@ -3,7 +3,7 @@
</template> </template>
<script> <script>
import Api from "../../components/API" import Api from "../../API"
const axisOptions = { const axisOptions = {
labels: { labels: {
@ -40,7 +40,7 @@
async created() { async created() {
await this.chartHits() await this.chartHits()
}, },
data () { data() {
return { return {
ready: false, ready: false,
data: [], data: [],
@ -122,16 +122,16 @@
}, },
methods: { methods: {
async chartHits() { async chartHits() {
const start = this.ago((3600 * 24) * 7) const start = this.nowSubtract((3600 * 24) * 7)
this.data = await Api.service_hits(this.service.id, start, this.now(), "hour") this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
this.series = [{ this.series = [{
name: this.service.name, name: this.service.name,
...this.data ...this.data
}] }]
this.ready = true this.ready = true
} }
}, }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -33,7 +33,7 @@
<script> <script>
import ServiceChart from "./ServiceChart"; import ServiceChart from "./ServiceChart";
import Api from "../API"; import Api from "../../API";
export default { export default {
name: 'ServiceFailures', name: 'ServiceFailures',

View File

@ -0,0 +1,84 @@
<template>
<apexchart v-if="ready" width="100%" height="300" type="heatmap" :options="chartOptions" :series="series"></apexchart>
</template>
<script>
import Api from "../../API"
export default {
name: 'ServiceHeatmap',
props: {
service: {
type: Object,
required: true
}
},
async created() {
await this.chartHeatmap()
},
data() {
return {
ready: false,
data: [],
chartOptions: {
chart: {
height: "100%",
width: "100%",
type: 'heatmap',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false,
},
enableShades: true,
shadeIntensity: 0.5,
colors: ["#d53a3b"],
series: [{data: [{}]}],
yaxis: {
labels: {
formatter: (value) => {
return value
},
},
},
tooltip: {
enabled: true,
x: {
show: false,
},
y: {
formatter: function(val, opts) { return val+" Failures" },
title: {
formatter: (seriesName) => seriesName,
},
},
}
},
series: [{
data: []
}]
}
},
methods: {
async chartHeatmap() {
const start = this.nowSubtract((3600 * 24) * 7)
const data = await Api.service_heatmap(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
let dataArr = []
data.forEach(function(d) {
let date = new Date(d.date);
dataArr.push({name: date.toLocaleString('en-us', { month: 'long' }), data: d.data});
});
this.series = dataArr
this.ready = true
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -16,15 +16,16 @@
</div> </div>
</div> </div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light"> <span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
Failed {{duration(current(), failure.created_at)}}<br> Failed {{ago(parseTime(failure.created_at))}}<br>
{{failure.issue}} {{failure.issue}}
</span> </span>
</div> </div>
</template> </template>
<script> <script>
import ServiceSparkLine from "./ServiceSparkLine"; import ServiceSparkLine from "./ServiceSparkLine";
import Api from "../API"; import Api from "../../API";
export default { export default {
name: 'ServiceInfo', name: 'ServiceInfo',
@ -37,7 +38,7 @@
required: true required: true
} }
}, },
data () { data() {
return { return {
set1: [], set1: [],
set2: [], set2: [],
@ -47,7 +48,7 @@
failures: null failures: null
} }
}, },
async mounted () { async mounted() {
this.set1 = await this.getHits(24 * 2, "hour") this.set1 = await this.getHits(24 * 2, "hour")
this.set1_name = this.calc(this.set1) this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24 * 7, "hour") this.set2 = await this.getHits(24 * 7, "hour")
@ -55,19 +56,19 @@
this.loaded = true this.loaded = true
}, },
methods: { methods: {
async getHits (hours, group) { async getHits(hours, group) {
const start = this.ago(3600 * hours) const start = this.nowSubtract(3600 * hours)
if (!this.service.online) { if (!this.service.online) {
this.failures = await Api.service_failures(this.service.id, this.now()-360, this.now(), 5) this.failures = await Api.service_failures(this.service.id, this.toUnix(start), this.toUnix(this.now()), 5)
return [ { name: "None", data: [] } ] return [{name: "None", data: []}]
} }
const data = await Api.service_hits(this.service.id, start, this.now(), group) const data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group)
if (!data) { if (!data) {
return [ { name: "None", data: [] } ] return [{name: "None", data: []}]
} }
return [ { name: "Latency", data: data.data } ] return [{name: "Latency", data: data.data}]
}, },
calc (s) { calc(s) {
let data = s[0].data let data = s[0].data
if (data.length > 1) { if (data.length > 1) {
let total = 0 let total = 0
@ -75,7 +76,7 @@
total += f.y total += f.y
}); });
total = total / data.length total = total / data.length
return total.toFixed(0) + "ms Average" return total.toFixed(0) + "ms"
} else { } else {
return "Offline" return "Offline"
} }

View File

@ -0,0 +1,32 @@
<template>
<div class="row stats_area mt-5 mb-4">
<div class="col-4">
<span class="lg_number">{{service.avg_response}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{service.online_24_hours}}%</span>
Uptime last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{service.online_7_days}}%</span>
Uptime last 7 Days
</div>
</div>
</template>
<script>
export default {
name: 'ServiceTopStats',
props: {
service: {
type: Object,
required: true
},
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,14 +0,0 @@
class Time {
now () {
return new Date();
}
utc () {
return new Date().getUTCDate();
}
utcToLocal (utc) {
let u = new Date().setUTCDate(utc)
return u.toLocaleString()
}
}
const time = new Time()
export default time

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveCheckin"> <form @submit.prevent="saveCheckin">
<div class="form-group row"> <div class="form-group row">
<div class="col-md-3"> <div class="col-md-3">
<label for="checkin_interval" class="col-form-label">Checkin Name</label> <label for="checkin_interval" class="col-form-label">Checkin Name</label>
@ -14,14 +14,14 @@
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10"> <input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div> </div>
<div class="col-3"> <div class="col-3">
<button @click="saveCheckin" type="submit" id="submit" class="btn btn-success d-block" style="margin-top: 14px;">Save Checkin</button> <button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block" style="margin-top: 14px;">Save Checkin</button>
</div> </div>
</div> </div>
</form> </form>
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'Checkin', name: 'Checkin',
@ -31,7 +31,7 @@
required: true required: true
} }
}, },
data () { data() {
return { return {
checkin: { checkin: {
name: "", name: "",
@ -45,15 +45,14 @@
}, },
methods: { methods: {
async saveCheckin(e) { async saveCheckin() {
e.preventDefault();
const data = {name: this.group.name, public: this.group.public} const data = {name: this.group.name, public: this.group.public}
await Api.group_create(data) await Api.group_create(data)
const groups = await Api.groups() const groups = await Api.groups()
this.$store.commit('setGroups', groups) this.$store.commit('setGroups', groups)
}, },
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveSettings"> <form @submit.prevent="saveSettings">
<div class="form-group"> <div class="form-group">
<label>Project Name</label> <label>Project Name</label>
<input v-model="core.name" type="text" class="form-control" placeholder="Great Uptime"> <input v-model="core.name" type="text" class="form-control" placeholder="Great Uptime">
@ -68,20 +68,9 @@
</select> </select>
</div> </div>
<div class="form-group"> <button @click.prevent="saveSettings" type="submit" class="btn btn-primary btn-block">Save Settings</button>
<div class="col-12">
<label class="d-none d-sm-block">Send Updates only</label>
<span class="switch">
<input v-model="core.update_notify" @change="core.update_notify = !!core.update_notify" type="checkbox" class="switch" id="switch-update_notify" v-bind:checked="core.update_notify">
<label for="switch-update_notify" class="mt-2 mt-sm-0"></label>
<small class="form-text text-muted">Enabling this will send only notifications when the status of a services changes.</small>
</span>
</div>
</div>
<button @click="saveSettings" type="submit" class="btn btn-primary btn-block">Save Settings</button> <div class="form-group row mt-5">
<div class="form-group row mt-3">
<label class="col-sm-3 col-form-label">API Key</label> <label class="col-sm-3 col-form-label">API Key</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input v-model="core.api_key" @focus="$event.target.select()" type="text" class="form-control select-input" readonly> <input v-model="core.api_key" @focus="$event.target.select()" type="text" class="form-control select-input" readonly>
@ -102,30 +91,31 @@
</template> </template>
<script> <script>
import Api from '../components/API' import Api from '../API'
export default { export default {
name: 'CoreSettings', name: 'CoreSettings',
data () { data() {
return { return {
core: this.$store.getters.core, core: this.$store.getters.core,
} }
}, },
async mounted () { async mounted() {
}, },
methods: { methods: {
async saveSettings (e) { async saveSettings() {
e.preventDefault()
const c = this.core const c = this.core
const coreForm = {name: c.name, description: c.description, domain: c.domain, const coreForm = {
timezone: c.timezone, using_cdn: c.using_cdn, footer: c.footer, update_notify: c.update_notify} name: c.name, description: c.description, domain: c.domain,
timezone: c.timezone, using_cdn: c.using_cdn, footer: c.footer, update_notify: c.update_notify
}
await Api.core_save(coreForm) await Api.core_save(coreForm)
const core = await Api.core() const core = await Api.core()
this.$store.commit('setCore', core) this.$store.commit('setCore', core)
this.core = core this.core = core
}, },
async renewApiKeys () { async renewApiKeys() {
let r = confirm("Are you sure you want to reset the API keys?"); let r = confirm("Are you sure you want to reset the API keys?");
if (r === true) { if (r === true) {
await Api.renewApiKeys() await Api.renewApiKeys()
@ -138,7 +128,7 @@
this.$refs.input.select(); this.$refs.input.select();
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -38,7 +38,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormGroup', name: 'FormGroup',
@ -50,7 +50,7 @@
type: Function type: Function
} }
}, },
data () { data() {
return { return {
loading: false, loading: false,
group: { group: {
@ -96,7 +96,7 @@
this.edit(false) this.edit(false)
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -59,7 +59,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormIntegration', name: 'FormIntegration',
@ -68,18 +68,23 @@
type: Object type: Object
} }
}, },
data () { data() {
return { return {
out: {}, out: {},
services: [] services: []
} }
}, },
watch: { watch: {},
},
methods: { methods: {
async addService(s) { async addService(s) {
const data = {name: s.name, type: s.type, domain: s.domain, port: s.port, check_interval: s.check_interval, timeout: s.timeout} const data = {
name: s.name,
type: s.type,
domain: s.domain,
port: s.port,
check_interval: s.check_interval,
timeout: s.timeout
}
const out = await Api.service_create(data) const out = await Api.service_create(data)
const services = await Api.services() const services = await Api.services()
this.$store.commit('setServices', services) this.$store.commit('setServices', services)
@ -97,7 +102,7 @@
this.$store.commit('setIntegrations', integrations) this.$store.commit('setIntegrations', integrations)
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -26,11 +26,11 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormLogin', name: 'FormLogin',
data () { data() {
return { return {
username: "", username: "",
password: "", password: "",
@ -48,7 +48,7 @@
this.disabled = false this.disabled = false
} }
}, },
async login () { async login() {
this.loading = true this.loading = true
this.error = false this.error = false
const auth = await Api.login(this.username, this.password) const auth = await Api.login(this.username, this.password)
@ -62,7 +62,7 @@
this.loading = false this.loading = false
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -24,10 +24,10 @@
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label> <label class="col-sm-4 col-form-label">Message Date Range</label>
<div class="col-sm-4"> <div class="col-sm-4">
<flatPickr v-model="message.start_on" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required /> <flatPickr v-model="message.start_on" @on-change="startChange" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required />
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<flatPickr v-model="message.end_on" :config="config" type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required /> <flatPickr v-model="message.end_on" @on-change="endChange" :config="config" type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required />
</div> </div>
</div> </div>
@ -90,7 +90,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
import flatPickr from 'vue-flatpickr-component'; import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css'; import 'flatpickr/dist/flatpickr.css';
@ -135,6 +135,12 @@
} }
}, },
methods: { methods: {
startChange(e) {
window.console.log(e)
},
endChange(e) {
window.console.log(e)
},
removeEdit() { removeEdit() {
this.message = {} this.message = {}
this.edit(false) this.edit(false)

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveNotifier"> <form @submit.prevent="saveNotifier">
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div> <div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
@ -38,13 +38,13 @@
</div> </div>
<div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0"> <div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click="saveNotifier" type="submit" class="btn btn-block text-capitalize" :class="{'btn-primary': !saved, 'btn-success': saved}"> <button @click.prevent="saveNotifier" type="submit" class="btn btn-block text-capitalize" :class="{'btn-primary': !saved, 'btn-success': saved}">
<i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save"}} <i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save"}}
</button> </button>
</div> </div>
<div class="col-12 col-sm-12 mt-3"> <div class="col-12 col-sm-12 mt-3">
<button @click="testNotifier" class="btn btn-secondary btn-block text-capitalize col-12 float-right"><i class="fa fa-vial"></i> <button @click.prevent="testNotifier" class="btn btn-secondary btn-block text-capitalize col-12 float-right"><i class="fa fa-vial"></i>
{{loading ? "Loading..." : "Test Notifier"}}</button> {{loading ? "Loading..." : "Test Notifier"}}</button>
</div> </div>
@ -57,7 +57,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'Notifier', name: 'Notifier',
@ -67,7 +67,7 @@ export default {
required: true required: true
} }
}, },
data () { data() {
return { return {
loading: false, loading: false,
error: null, error: null,
@ -79,8 +79,7 @@ export default {
}, },
methods: { methods: {
async saveNotifier(e) { async saveNotifier() {
e.preventDefault();
this.loading = true this.loading = true
let form = {} let form = {}
this.notifier.form.forEach((f) => { this.notifier.form.forEach((f) => {
@ -91,15 +90,14 @@ export default {
form.method = this.notifier.method form.method = this.notifier.method
await Api.notifier_save(form) await Api.notifier_save(form)
const notifiers = await Api.notifiers() const notifiers = await Api.notifiers()
this.$store.commit('setNotifiers', notifiers) await this.$store.commit('setNotifiers', notifiers)
this.saved = true this.saved = true
this.loading = false this.loading = false
setTimeout(() => { setTimeout(() => {
this.saved = false this.saved = false
}, 2000) }, 2000)
}, },
async testNotifier(e) { async testNotifier() {
e.preventDefault();
this.ok = false this.ok = false
this.loading = true this.loading = true
let form = {} let form = {}

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveService"> <form @submit.prevent="saveService">
<h4 class="mb-5 text-muted">Basic Information</h4> <h4 class="mb-5 text-muted">Basic Information</h4>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">Service Name</label> <label class="col-sm-4 col-form-label">Service Name</label>
@ -12,10 +12,10 @@
<label for="service_type" class="col-sm-4 col-form-label">Service Type</label> <label for="service_type" class="col-sm-4 col-form-label">Service Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select v-model="service.type" class="form-control" id="service_type" > <select v-model="service.type" class="form-control" id="service_type" >
<option value="http" >HTTP Service</option> <option value="http">HTTP Service</option>
<option value="tcp" >TCP Service</option> <option value="tcp">TCP Service</option>
<option value="udp" >UDP Service</option> <option value="udp">UDP Service</option>
<option value="icmp" >ICMP Ping</option> <option value="icmp">ICMP Ping</option>
</select> </select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small> <small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
</div> </div>
@ -111,15 +111,8 @@
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small> <small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">List Order</label>
<div class="col-sm-8">
<input v-model="service.order" type="number" name="order" class="form-control" min="0" id="order">
<small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small>
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row"> <div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Verify SSL</label> <label class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1"> <div class="col-8 mt-1">
<span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left"> <span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left">
<input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl"> <input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl">
@ -128,7 +121,7 @@
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Notifications</label> <label class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1"> <div class="col-8 mt-1">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left"> <span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications"> <input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
@ -136,8 +129,17 @@
</span> </span>
</div> </div>
</div> </div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify All Changes</label>
<div class="col-8 mt-1">
<span @click="service.notify_all_changes = !!service.notify_all_changes" class="switch float-left">
<input v-model="service.notify_all_changes" type="checkbox" name="notify_all-option" class="switch" id="notify_all" v-bind:checked="service.notify_all_changes">
<label for="notify_all">Continuously notify when service is failing.</label>
</span>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Visible</label> <label class="col-sm-4 col-form-label">Visible</label>
<div class="col-8 mt-1"> <div class="col-8 mt-1">
<span @click="service.public = !!service.public" class="switch float-left"> <span @click="service.public = !!service.public" class="switch float-left">
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public"> <input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public">
@ -157,7 +159,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormService', name: 'FormService',
@ -181,6 +183,7 @@
order: 1, order: 1,
verify_ssl: true, verify_ssl: true,
allow_notifications: true, allow_notifications: true,
notify_all_changes: true,
public: true, public: true,
}, },
groups: [], groups: [],
@ -203,8 +206,7 @@
} }
}, },
methods: { methods: {
async saveService(e) { async saveService() {
e.preventDefault()
let s = this.service let s = this.service
delete s.failures delete s.failures
delete s.created_at delete s.created_at

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="container col-md-7 col-sm-12 mt-2 sm-container"> <div class="container col-md-7 col-sm-12 mt-2 sm-container">
<div class="col-12 col-md-8 offset-md-2 mb-4"> <div class="col-12 col-md-8 offset-md-2 mb-4">
<img class="col-12 mt-5 mt-md-0" src="/public/banner.png"> <img class="col-12 mt-5 mt-md-0" src="/banner.png">
</div> </div>
<div class="col-12"> <div class="col-12">
@ -96,7 +96,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
import Index from "../pages/Index"; import Index from "../pages/Index";
export default { export default {

View File

@ -12,7 +12,7 @@
required: true required: true
} }
}, },
data () { data() {
return { return {
icon: "toggle-on", icon: "toggle-on",
running: true running: true
@ -37,7 +37,7 @@
this.running = !this.running this.running = !this.running
}, },
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -55,7 +55,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormUser', name: 'FormUser',

View File

@ -12,7 +12,6 @@ import "./icons"
Vue.component('apexchart', VueApexCharts) Vue.component('apexchart', VueApexCharts)
Vue.use(VueRouter); Vue.use(VueRouter);
Vue.use(require('vue-moment'));
Vue.config.productionTip = false Vue.config.productionTip = false
new Vue({ new Vue({

View File

@ -1,45 +1,51 @@
import Vue from "vue"; import Vue from "vue";
const { zonedTimeToUtc, utcToZonedTime, subSeconds, parse, parseISO, getUnixTime, fromUnixTime, format, differenceInSeconds, formatDistanceToNow, formatDistance } = require('date-fns')
export default Vue.mixin({ export default Vue.mixin({
methods: { methods: {
now() { now() {
return Math.round(new Date().getTime() / 1000)
},
current() {
return new Date() return new Date()
}, },
ago(seconds) { current() {
return this.now() - seconds return parse(new Date())
},
utc(val) {
return fromUnixTime(this.toUnix(val) + val.getTimezoneOffset() * 60 * 1000)
},
ago(t1) {
return formatDistanceToNow(t1)
},
nowSubtract(seconds) {
return subSeconds(new Date(), seconds)
}, },
duration(t1, t2) { duration(t1, t2) {
const val = (this.toUnix(t1) - this.toUnix(t2)) return formatDistance(t1, t2)
if (val <= 59) {
return this.$moment.duration(val, 'seconds').get('seconds') + " seconds ago"
}
return this.$moment.duration(val, 'seconds').humanize();
}, },
niceDate(val) { niceDate(val) {
return this.parseTime(val).format('LLLL') return this.parseTime(val).format('LLLL')
}, },
parseTime(val) { parseTime(val) {
return this.$moment(val, this.$moment.ISO_8601, true) return parseISO(val)
}, },
toLocal(val, suf='at') { toLocal(val, suf = 'at') {
return this.parseTime(val).local().format(`dddd, MMM Do \\${suf} h:mma`) const t = this.parseTime(val)
return format(t, `EEEE, MMM do h:mma`)
}, },
toUnix(val) { toUnix(val) {
return this.$moment(val).utc().unix().valueOf() return getUnixTime(val)
}, },
fromUnix(val) { fromUnix(val) {
return this.$moment.unix(val).utc() return fromUnixTime(val)
}, },
isBetween(t1, t2) { isBetween(t1, t2) {
const now = this.$moment(t1).utc().valueOf() return differenceInSeconds(parseISO(t1), parseISO(t2)) > 0
const sub = this.$moment(t2).utc().valueOf() },
return (now - sub) > 0 hour() {
return 3600
},
day() {
return 3600 * 24
}, },
hour(){ return 3600 },
day() { return 3600 * 24 },
serviceLink(service) { serviceLink(service) {
if (!service) { if (!service) {
return "" return ""

View File

@ -6,7 +6,7 @@
</template> </template>
<script> <script>
import Api from "../components/API" import Api from "../API"
import TopNav from "../components/Dashboard/TopNav"; import TopNav from "../components/Dashboard/TopNav";
export default { export default {

View File

@ -22,7 +22,7 @@
</template> </template>
<script> <script>
import Api from '../components/API'; import Api from '../API';
import Group from '../components/Index/Group'; import Group from '../components/Index/Group';
import Header from '../components/Index/Header'; import Header from '../components/Index/Header';
import MessageBlock from '../components/Index/MessageBlock'; import MessageBlock from '../components/Index/MessageBlock';
@ -37,7 +37,7 @@ export default {
Group, Group,
Header Header
}, },
data () { data() {
return { return {
logged_in: false logged_in: false
} }

View File

@ -12,7 +12,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
import FormLogin from '../forms/Login'; import FormLogin from '../forms/Login';
export default { export default {

View File

@ -3,34 +3,30 @@
<p v-if="logs.length === 0" class="text-monospace sm"> <p v-if="logs.length === 0" class="text-monospace sm">
Loading Logs... Loading Logs...
</p> </p>
<p v-for="(log, index) in logs.reverse()" class="text-monospace sm">{{log}}</p> <p v-for="(log, index) in logs" class="text-monospace sm">{{log}}</p>
</div> </div>
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'Logs', name: 'Logs',
components: { components: {},
data() {
},
data () {
return { return {
logs: [], logs: [],
last: "", last: "",
t: null t: null
} }
}, },
created() { async created() {
await this.getLogs()
if (!this.t) { if (!this.t) {
this.t = setInterval(() => { this.t = setInterval(async () => {
this.lastLog() await this.lastLog()
}, 650) }, 650)
} }
},
async mounted() {
await this.getLogs()
}, },
beforeDestroy() { beforeDestroy() {
clearInterval(this.t) clearInterval(this.t)
@ -43,20 +39,18 @@ export default {
}, },
async getLogs() { async getLogs() {
const logs = await Api.logs() const logs = await Api.logs()
this.logs = logs.reverse() const l = logs.reverse()
this.last = this.cleanLog(this.logs[this.logs.length-1]) this.last = this.cleanLog(l[l.length - 1])
this.logs = l
}, },
async lastLog() { async lastLog() {
const log = await Api.logs_last() const log = await Api.logs_last()
const cleanLast = this.cleanLog(log) const cleanLast = this.cleanLog(log)
if (this.last !== cleanLast) { if (this.last !== cleanLast) {
this.last = cleanLast this.last = cleanLast
this.logs.reverse().push(log) this.logs.reverse().push(log)
} }
} }
} }
} }
</script> </script>

View File

@ -7,50 +7,37 @@
{{service.online ? "ONLINE" : "OFFLINE"}} {{service.online ? "ONLINE" : "OFFLINE"}}
</span> </span>
<h4 class="mt-2"><router-link to="/">{{$store.getters.core.name}}</router-link> - {{service.name}} <h4 class="mt-2">
<router-link to="/">{{$store.getters.core.name}}</router-link> - {{service.name}}
<span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}"> <span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}} {{service.online ? "ONLINE" : "OFFLINE"}}
</span> </span>
</h4> </h4>
<div class="row stats_area mt-5 mb-5"> <ServiceTopStats :service="service"/>
<div class="col-4">
<span class="lg_number">{{service.online_24_hours}}%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">31ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">85.70%</span>
Total Uptime
</div>
</div>
<div v-for="(message, index) in messages" v-if="messageInRange(message)"> <div v-for="(message, index) in messages" v-if="messageInRange(message)">
<MessageBlock :message="message"/> <MessageBlock :message="message"/>
</div> </div>
<div class="row mt-5 mb-4">
<span class="col-6 font-2">
<flatPickr v-model="start_time" type="text" name="start_time" class="form-control form-control-plaintext" id="start_time" value="0001-01-01T00:00:00Z" required />
</span>
<span class="col-6 font-2">
<flatPickr v-model="end_time" type="text" name="end_time" class="form-control form-control-plaintext" id="end_time" value="0001-01-01T00:00:00Z" required />
</span>
</div>
<div v-if="series" class="service-chart-container"> <div v-if="series" class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart> <apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart>
</div> </div>
<div v-if="series" class="service-chart-heatmap"> <div class="service-chart-heatmap mt-3 mb-4">
<apexchart width="100%" height="215" type="heatmap" :options="chartOptions" :series="series"></apexchart> <ServiceHeatmap :service="service"/>
</div> </div>
<form id="service_date_form" class="col-12 mt-2 mb-3"> <nav v-if="service.failures" class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs">
<input type="text" class="d-none" name="start" id="service_start" data-input>
<span data-toggle title="toggle" id="start_date" class="text-muted small float-left pointer mt-2">Thu, 09 Jan 2020 to Thu, 16 Jan 2020</span>
<button type="submit" class="btn btn-light btn-sm mt-2">Set Timeframe</button>
<input type="text" class="d-none" name="end" id="service_end" data-input>
<div id="start_container"></div>
<div id="end_container"></div>
</form>
<nav class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs">
<a @click="tab='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a> <a @click="tab='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a>
<a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a> <a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a>
<a @click="tab='checkins'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Checkins</a> <a @click="tab='checkins'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Checkins</a>
@ -58,7 +45,7 @@
</nav> </nav>
<div class="tab-content"> <div v-if="service.failures" class="tab-content">
<div class="tab-pane fade active show"> <div class="tab-pane fade active show">
<ServiceFailures :service="service"/> <ServiceFailures :service="service"/>
</div> </div>
@ -98,10 +85,14 @@
</template> </template>
<script> <script>
import Api from "../components/API" import Api from "../API"
import MessageBlock from '../components/Index/MessageBlock'; import MessageBlock from '../components/Index/MessageBlock';
import ServiceFailures from '../components/Service/ServiceFailures'; import ServiceFailures from '../components/Service/ServiceFailures';
import Checkin from "../forms/Checkin"; import Checkin from "../forms/Checkin";
import ServiceHeatmap from "@/components/Service/ServiceHeatmap";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
const axisOptions = { const axisOptions = {
labels: { labels: {
@ -130,11 +121,14 @@
export default { export default {
name: 'Service', name: 'Service',
components: { components: {
ServiceTopStats,
ServiceHeatmap,
ServiceFailures, ServiceFailures,
MessageBlock, MessageBlock,
Checkin Checkin,
flatPickr
}, },
data () { data() {
return { return {
id: null, id: null,
tab: "failures", tab: "failures",
@ -144,6 +138,8 @@ export default {
data: null, data: null,
messages: [], messages: [],
failures: [], failures: [],
start_time: "",
end_time: "",
chartOptions: { chartOptions: {
chart: { chart: {
height: 500, height: 500,
@ -218,7 +214,10 @@ export default {
series: [{ series: [{
data: [] data: []
}], }],
heatmap_data: [] heatmap_data: [],
config: {
enableTime: true
},
} }
}, },
async mounted() { async mounted() {
@ -242,22 +241,16 @@ export default {
}, },
async getService(s) { async getService(s) {
await this.chartHits() await this.chartHits()
await this.heatmapData()
await this.serviceFailures() await this.serviceFailures()
}, },
async serviceFailures() { async serviceFailures() {
this.failures = await Api.service_failures(this.service.id, this.now() - 3600, this.now(), 15) this.failures = await Api.service_failures(this.service.id, this.now() - 3600, this.now(), 15)
}, },
async chartHits() { async chartHits() {
this.data = await Api.service_hits(this.service.id, 0, 99999999999, "hour") const start = this.nowSubtract((3600 * 24) * 7)
this.series = [{ this.start_time = start
name: this.service.name, this.end_time = new Date()
...this.data this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
}]
this.ready = true
},
async heatmapData() {
this.data = await Api.service_heatmap(this.service.id, 0, 99999999999, "hour")
this.series = [{ this.series = [{
name: this.service.name, name: this.service.name,
...this.data ...this.data

View File

@ -42,7 +42,7 @@
<div class="row align-content-center"> <div class="row align-content-center">
<img class="rounded text-center" width="300" height="300" :src="qrcode"> <img class="rounded text-center" width="300" height="300" :src="qrcode">
</div> </div>
<a class="btn btn-sm btn-primary" href=statping://setup?domain&#61;https://demo.statping.com&amp;api&#61;6b05b48f4b3a1460f3864c31b26cab6a27dbaff9>Open in Statping App</a> <a class="btn btn-sm btn-primary" :href="qrurl">Open in Statping App</a>
<a href="settings/export" class="btn btn-sm btn-secondary">Export Settings</a> <a href="settings/export" class="btn btn-sm btn-secondary">Export Settings</a>
</div> </div>
</div> </div>
@ -53,32 +53,11 @@
</div> </div>
<div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-style-tab'), show: liClass('v-pills-style-tab')}" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab"> <div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-style-tab'), show: liClass('v-pills-style-tab')}" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab">
<ThemeEditor :core="core"/> <ThemeEditor :core="core"/>
</div> </div>
<div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-cache-tab'), show: liClass('v-pills-cache-tab')}" id="v-pills-cache" role="tabpanel" aria-labelledby="v-pills-cache-tab"> <div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-cache-tab'), show: liClass('v-pills-cache-tab')}" id="v-pills-cache" role="tabpanel" aria-labelledby="v-pills-cache-tab">
<h3>Cache</h3> <Cache/>
<table class="table">
<thead>
<tr>
<th scope="col">URL</th>
<th scope="col">Size</th>
<th scope="col">Expiration</th>
</tr>
</thead>
<tbody>
<tr v-for="(cache, index) in cache">
<td>{{cache.url}}</td>
<td>{{cache.size}}</td>
<td>{{expireTime(cache.expiration)}}</td>
</tr>
</tbody>
</table>
<a @click.prevent="clearCache" href="#" class="btn btn-danger btn-block">Clear Cache</a>
</div> </div>
<div v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.title}_${index}`" class="tab-pane fade" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), show: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" role="tabpanel" v-bind:aria-labelledby="`v-pills-${notifier.method.toLowerCase()}-tab`"> <div v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.title}_${index}`" class="tab-pane fade" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), show: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" role="tabpanel" v-bind:aria-labelledby="`v-pills-${notifier.method.toLowerCase()}-tab`">
@ -97,54 +76,50 @@
</template> </template>
<script> <script>
import Api from '../components/API'; import Api from '../API';
import CoreSettings from '../forms/CoreSettings'; import CoreSettings from '../forms/CoreSettings';
import FormIntegration from '../forms/Integration'; import FormIntegration from '../forms/Integration';
import Notifier from "../forms/Notifier"; import Notifier from "../forms/Notifier";
import ThemeEditor from "../components/Dashboard/ThemeEditor"; import ThemeEditor from "../components/Dashboard/ThemeEditor";
import Cache from "@/components/Dashboard/Cache";
export default { export default {
name: 'Settings', name: 'Settings',
components: { components: {
Cache,
ThemeEditor, ThemeEditor,
FormIntegration, FormIntegration,
Notifier, Notifier,
CoreSettings CoreSettings
}, },
data () { data() {
return { return {
tab: "v-pills-home-tab", tab: "v-pills-home-tab",
qrcode: "", qrcode: "",
core: this.$store.getters.core, qrurl: "",
cache: [], core: this.$store.getters.core
} }
}, },
async mounted () { async mounted() {
this.cache = await Api.cache() this.cache = await Api.cache()
}, },
async created() { async created() {
const qrurl = `statping://setup?domain=${core.domain}&api=${core.api_secret}` const c = this.$store.getters.core
this.qrcode = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=" + encodeURI(qrurl) this.qrurl = `statping://setup?domain=${c.domain}&api=${c.api_secret}`
this.qrcode = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=" + encodeURI(this.qrurl)
}, },
beforeMount() { beforeMount() {
}, },
methods: { methods: {
changeTab (e) { changeTab(e) {
this.tab = e.target.id this.tab = e.target.id
}, },
liClass (id) { liClass(id) {
return this.tab === id return this.tab === id
},
expireTime(ex) {
return this.toLocal(ex)
},
async clearCache () {
await Api.clearCache()
this.cache = await Api.cache()
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -13,7 +13,7 @@ import Service from "./pages/Service";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import Setup from "./forms/Setup"; import Setup from "./forms/Setup";
import Api from "./components/API"; import Api from "./API";
const routes = [ const routes = [
{ {

View File

@ -1,6 +1,6 @@
import Vuex from 'vuex' import Vuex from 'vuex'
import Vue from 'vue' import Vue from 'vue'
import Api from "./components/API" import Api from "./API"
Vue.use(Vuex) Vue.use(Vuex)
@ -105,7 +105,7 @@ export default new Vuex.Store({
} }
}, },
actions: { actions: {
async loadRequired (context) { async loadRequired(context) {
const core = await Api.core() const core = await Api.core()
context.commit("setCore", core); context.commit("setCore", core);
const groups = await Api.groups() const groups = await Api.groups()
@ -125,7 +125,7 @@ export default new Vuex.Store({
// } // }
window.console.log('finished loading required data') window.console.log('finished loading required data')
}, },
async loadAdmin (context) { async loadAdmin(context) {
const core = await Api.core() const core = await Api.core()
context.commit("setCore", core); context.commit("setCore", core);
const groups = await Api.groups() const groups = await Api.groups()

16
frontend/test/API.spec.js Normal file
View File

@ -0,0 +1,16 @@
import API from "@/API"
import { shallowMount, mount } from '@vue/test-utils'
describe('API Tests', async () => {
await it('should get core info', async () => {
const wrapper = mount(API)
const core = await wrapper.core()
expect(core).toBe(9)
})
});

View File

@ -0,0 +1,24 @@
// import Vuex from 'vuex'
// import Api from '../src/components/API'
// import thisStore from '../src/store'
//
// import { createLocalVue } from '@vue/test-utils'
// const localVue = createLocalVue()
// localVue.use(Vuex)
//
// describe('MyName test', async () => {
// const services = [
// { id: 1, title: 'Apple', order_id: 3 },
// { id: 2, title: 'Orange', order_id: 2},
// { id: 3, title: 'Carrot', order_id: 1}
// ]
//
// const store = new Vuex.Store(thisStore)
//
// await store.dispatch('loadRequired')
//
// console.log(store.getters.services)
//
// expect(store.getters.services).toEqual(services.slice(0, 20))
// })

View File

@ -0,0 +1,32 @@
import { shallowMount, mount } from '@vue/test-utils'
import FormLogin from "../../src/forms/Login.vue"
const wrapper = shallowMount(FormLogin)
describe('Login Form', () => {
it('has a created hook', () => {
expect(typeof FormLogin.methods.checkForm).toBe('function')
})
it('should login', async () => {
expect(wrapper.vm.$data.loading).toBe(false)
expect(wrapper.vm.$data.username).toBe('')
expect(wrapper.vm.$data.password).toBe('')
wrapper.setData({ username: 'admin' })
wrapper.setData({ password: 'admin' })
wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.$data.loading).toBe(true)
done()
})
});

View File

@ -0,0 +1,17 @@
// import { mount } from '@vue/test-utils';
// import Index from '../../src/pages/Index';
//
// const wrapper = mount(Index);
// describe('MyName test', () => {
// it('Displays my name when I write it', () => {
//
// expect(wrapper.vm.$data.logged_in).toBe('My name');
//
// const input = wrapper.find('input');
// input.element.value = 'Stefan';
// input.trigger('input');
//
// expect(wrapper.vm.$data.name).toBe('Stefan');
// })
// });

5
frontend/test/setup.js Normal file
View File

@ -0,0 +1,5 @@
require('jsdom-global')()
global.expect = require('expect')

File diff suppressed because it is too large Load Diff

View File

@ -89,7 +89,6 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
if c.Timezone != app.Timezone { if c.Timezone != app.Timezone {
app.Timezone = c.Timezone app.Timezone = c.Timezone
} }
app.UpdateNotify = c.UpdateNotify
app.UseCdn = types.NewNullBool(c.UseCdn.Bool) app.UseCdn = types.NewNullBool(c.UseCdn.Bool)
core.CoreApp, err = core.UpdateCore(app) core.CoreApp, err = core.UpdateCore(app)
returnJson(core.CoreApp, w, r) returnJson(core.CoreApp, w, r)

View File

@ -25,6 +25,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
"USE_CDN": func() bool { "USE_CDN": func() bool {
return core.CoreApp.UseCdn.Bool return core.CoreApp.UseCdn.Bool
}, },
"USING_ASSETS": func() bool {
return core.CoreApp.UsingAssets()
},
"BasePath": func() string { "BasePath": func() string {
return basePath return basePath
}, },

View File

@ -70,9 +70,6 @@ func (u *discord) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
var msg interface{} var msg interface{}
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)

View File

@ -193,9 +193,6 @@ func (u *email) OnFailure(s *types.Service, f *types.Failure) {
func (u *email) OnSuccess(s *types.Service) { func (u *email) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
var msg string var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))

View File

@ -72,9 +72,6 @@ func (u *lineNotifier) OnFailure(s *types.Service, f *types.Failure) {
func (u *lineNotifier) OnSuccess(s *types.Service) { func (u *lineNotifier) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
var msg string var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))

View File

@ -99,9 +99,6 @@ func (u *mobilePush) OnSuccess(s *types.Service) {
data := dataJson(s, nil) data := dataJson(s, nil)
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
var msgStr string var msgStr string
if s.UpdateNotify {
s.UpdateNotify = false
}
msgStr = s.DownText msgStr = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))

View File

@ -92,9 +92,6 @@ func (u *telegram) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
var msg interface{} var msg interface{}
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)

View File

@ -102,9 +102,6 @@ func (u *twilio) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
var msg string var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)

View File

@ -41,7 +41,6 @@ type Core struct {
Setup bool `gorm:"-" json:"setup"` Setup bool `gorm:"-" json:"setup"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"` MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"` UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
UpdateNotify NullBool `gorm:"column:update_notify;default:true" json:"update_notify,omitempty"`
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"` Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
LoggedIn bool `gorm:"-" json:"logged_in"` LoggedIn bool `gorm:"-" json:"logged_in"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`

View File

@ -33,7 +33,6 @@ type Service struct {
Port int `gorm:"not null;column:port" json:"port" scope:"user,admin"` Port int `gorm:"not null;column:port" json:"port" scope:"user,admin"`
Timeout int `gorm:"default:30;column:timeout" json:"timeout" scope:"user,admin"` Timeout int `gorm:"default:30;column:timeout" json:"timeout" scope:"user,admin"`
Order int `gorm:"default:0;column:order_id" json:"order_id"` Order int `gorm:"default:0;column:order_id" json:"order_id"`
AllowNotifications NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications" scope:"user,admin"`
VerifySSL NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin"` VerifySSL NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin"`
Public NullBool `gorm:"default:true;column:public" json:"public"` Public NullBool `gorm:"default:true;column:public" json:"public"`
GroupId int `gorm:"default:0;column:group_id" json:"group_id"` GroupId int `gorm:"default:0;column:group_id" json:"group_id"`
@ -53,8 +52,9 @@ type Service struct {
Checkpoint time.Time `gorm:"-" json:"-"` Checkpoint time.Time `gorm:"-" json:"-"`
SleepDuration time.Duration `gorm:"-" json:"-"` SleepDuration time.Duration `gorm:"-" json:"-"`
LastResponse string `gorm:"-" json:"-"` LastResponse string `gorm:"-" json:"-"`
AllowNotifications NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications" scope:"user,admin"`
UserNotified bool `gorm:"-" json:"-"` // True if the User was already notified about a Downtime UserNotified bool `gorm:"-" json:"-"` // True if the User was already notified about a Downtime
UpdateNotify bool `gorm:"-" json:"-"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool` UpdateNotify NullBool `gorm:"default:true;column:notify_all_changes" json:"notify_all_changes" scope:"user,admin"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool`
DownText string `gorm:"-" json:"-"` // Contains the current generated Downtime Text DownText string `gorm:"-" json:"-"` // Contains the current generated Downtime Text
SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available
LastStatusCode int `gorm:"-" json:"status_code"` LastStatusCode int `gorm:"-" json:"status_code"`