From 5e60b5578b47d8608e2d19bcdac3ba53434c597d Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Mon, 31 Dec 2018 13:36:58 -0800 Subject: [PATCH] grouping - public option --- core/groups.go | 51 +++- core/sample.go | 14 + handlers/api.go | 6 + handlers/groups.go | 110 ++++++++ handlers/handlers.go | 8 +- handlers/routes.go | 7 + handlers/services.go | 2 +- source/tmpl/form_group.gohtml | 26 ++ source/tmpl/form_service.gohtml | 15 +- source/tmpl/postman.json | 484 ++++++++++++++++++++++++++++++++ source/tmpl/services.gohtml | 10 +- source/wiki.go | 2 +- 12 files changed, 713 insertions(+), 22 deletions(-) create mode 100644 handlers/groups.go create mode 100644 source/tmpl/form_group.gohtml diff --git a/core/groups.go b/core/groups.go index 06449371..5e01c4ae 100644 --- a/core/groups.go +++ b/core/groups.go @@ -1,10 +1,49 @@ package core -import "github.com/hunterlong/statping/types" +import ( + "github.com/hunterlong/statping/types" + "time" +) -// SelectGroups returns all messages -func SelectGroups() ([]*types.Group, error) { - var groups []*types.Group - db := groupsDb().Find(&groups).Order("id desc") - return groups, db.Error +type Group struct { + *types.Group +} + +// Delete will remove a group +func (g *Group) Delete() error { + err := messagesDb().Delete(g) + if err.Error != nil { + return err.Error + } + return err.Error +} + +// Update will update a group in the database +func (g *Group) Update() error { + err := servicesDB().Update(&g) + return err.Error +} + +// Create will create a group and insert it into the database +func (g *Group) Create() (int64, error) { + g.CreatedAt = time.Now() + db := groupsDb().Create(g) + return g.Id, db.Error +} + +// SelectGroups returns all groups +func SelectGroups() []*Group { + var groups []*Group + groupsDb().Find(&groups).Order("id desc") + return groups +} + +// SelectGroup returns a *core.Group +func SelectGroup(id int64) *Group { + for _, g := range SelectGroups() { + if g.Id == id { + return g + } + } + return nil } diff --git a/core/sample.go b/core/sample.go index 613e21f3..7de53968 100644 --- a/core/sample.go +++ b/core/sample.go @@ -86,11 +86,25 @@ func InsertSampleData() error { insertMessages() + insertSampleGroups() + utils.Log(1, "Sample data has finished importing") return nil } +func insertSampleGroups() error { + group1 := &Group{&types.Group{ + Name: "Main Services", + }} + _, err := group1.Create() + group2 := &Group{&types.Group{ + Name: "Linked Services", + }} + _, err = group2.Create() + return err +} + // insertSampleCheckins will create 2 checkins with 60 successful hits per Checkin func insertSampleCheckins() error { s1 := SelectService(1) diff --git a/handlers/api.go b/handlers/api.go index 16c1450d..95bbb3bc 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -88,6 +88,12 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht case *core.User: objName = "user" objId = v.Id + case *types.Group: + objName = "group" + objId = v.Id + case *core.Group: + objName = "group" + objId = v.Id case *core.Checkin: objName = "checkin" objId = v.Id diff --git a/handlers/groups.go b/handlers/groups.go new file mode 100644 index 00000000..9ee68852 --- /dev/null +++ b/handlers/groups.go @@ -0,0 +1,110 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package handlers + +import ( + "encoding/json" + "errors" + "github.com/gorilla/mux" + "github.com/hunterlong/statping/core" + "github.com/hunterlong/statping/utils" + "net/http" +) + +func apiAllGroupHandler(w http.ResponseWriter, r *http.Request) { + if !IsReadAuthenticated(r) { + sendUnauthorizedJson(w, r) + return + } + groups := core.SelectGroups() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(groups) +} + +func apiGroupHandler(w http.ResponseWriter, r *http.Request) { + if !IsReadAuthenticated(r) { + sendUnauthorizedJson(w, r) + return + } + vars := mux.Vars(r) + group := core.SelectGroup(utils.ToInt(vars["id"])) + if group == nil { + sendErrorJson(errors.New("group not found"), w, r) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(group) +} + +func apiCreateGroupHandler(w http.ResponseWriter, r *http.Request) { + if !IsFullAuthenticated(r) { + sendUnauthorizedJson(w, r) + return + } + var group *core.Group + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&group) + if err != nil { + sendErrorJson(err, w, r) + return + } + _, err = group.Create() + if err != nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(group, "create", w, r) +} + +func apiGroupUpdateHandler(w http.ResponseWriter, r *http.Request) { + if !IsFullAuthenticated(r) { + sendUnauthorizedJson(w, r) + return + } + vars := mux.Vars(r) + group := core.SelectGroup(utils.ToInt(vars["id"])) + if group == nil { + sendErrorJson(errors.New("group not found"), w, r) + return + } + decoder := json.NewDecoder(r.Body) + decoder.Decode(&group) + err := group.Update() + if err != nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(group, "update", w, r) +} + +func apiGroupDeleteHandler(w http.ResponseWriter, r *http.Request) { + if !IsFullAuthenticated(r) { + sendUnauthorizedJson(w, r) + return + } + vars := mux.Vars(r) + group := core.SelectGroup(utils.ToInt(vars["id"])) + if group == nil { + sendErrorJson(errors.New("group not found"), w, r) + return + } + err := group.Delete() + if err != nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(group, "delete", w, r) +} diff --git a/handlers/handlers.go b/handlers/handlers.go index f54a6bf2..342242d6 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -192,6 +192,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap "Services": func() []types.ServiceInterface { return core.CoreApp.Services }, + "Groups": func() []*core.Group { + return core.SelectGroups() + }, "len": func(g []types.ServiceInterface) int { return len(g) }, @@ -259,6 +262,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap "NewMessage": func() *types.Message { return new(types.Message) }, + "NewGroup": func() *types.Group { + return new(types.Group) + }, } } @@ -276,7 +282,7 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") } - templates := []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"} + templates := []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"} javascripts := []string{"charts.js", "chart_index.js"} render, err := source.TmplBox.String(file) diff --git a/handlers/routes.go b/handlers/routes.go index 2feaaab8..6e4e4fe6 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -85,6 +85,13 @@ func Router() *mux.Router { r.Handle("/service/{id}/edit", http.HandlerFunc(servicesViewHandler)) r.Handle("/service/{id}/delete_failures", http.HandlerFunc(servicesDeleteFailuresHandler)).Methods("GET") + // API GROUPS Routes + r.Handle("/api/groups", http.HandlerFunc(apiAllGroupHandler)).Methods("GET") + r.Handle("/api/groups", http.HandlerFunc(apiCreateGroupHandler)).Methods("POST") + r.Handle("/api/groups/{id}", http.HandlerFunc(apiGroupHandler)).Methods("GET") + r.Handle("/api/groups/{id}", http.HandlerFunc(apiGroupUpdateHandler)).Methods("POST") + r.Handle("/api/groups/{id}", http.HandlerFunc(apiGroupDeleteHandler)).Methods("DELETE") + // API Routes r.Handle("/api", http.HandlerFunc(apiIndexHandler)) r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler)) diff --git a/handlers/services.go b/handlers/services.go index 230de225..c74a8b46 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -53,7 +53,7 @@ func servicesHandler(w http.ResponseWriter, r *http.Request) { } data := map[string]interface{}{ "Services": core.CoreApp.Services, - "Groups": core.CoreApp.Services, + "Groups": core.SelectGroups(), } ExecuteResponse(w, r, "services.gohtml", data, nil) } diff --git a/source/tmpl/form_group.gohtml b/source/tmpl/form_group.gohtml new file mode 100644 index 00000000..9badb4cd --- /dev/null +++ b/source/tmpl/form_group.gohtml @@ -0,0 +1,26 @@ +{{define "form_group"}} +
+
+{{$message := .}} +{{if ne .Id 0}} +
+{{else}} + +{{end}} +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+{{end}} diff --git a/source/tmpl/form_service.gohtml b/source/tmpl/form_service.gohtml index 558fbf04..6ca2bbaf 100644 --- a/source/tmpl/form_service.gohtml +++ b/source/tmpl/form_service.gohtml @@ -104,20 +104,21 @@
- + + {{range Groups}} + + {{end}} - Use HTTP if you are checking a website or use TCP if you are checking a server + Attach this service to a group
- - + +
diff --git a/source/tmpl/postman.json b/source/tmpl/postman.json index d12e5c70..3407dbf9 100644 --- a/source/tmpl/postman.json +++ b/source/tmpl/postman.json @@ -769,6 +769,490 @@ } ] }, + { + "name": "Groups", + "item": [ + { + "name": "All Groups", + "event": [ + { + "listen": "test", + "script": { + "id": "d87f8a4e-7640-45b8-9d45-4f6e6f2463ee", + "exec": [ + "pm.test(\"View All Groups\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.length).to.eql(2);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{api_key}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{endpoint}}/api/groups", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "groups" + ] + }, + "description": "View an array of all Services added to your Statping instance." + }, + "response": [] + }, + { + "name": "View Group", + "event": [ + { + "listen": "test", + "script": { + "id": "023c5643-6cb1-4cd0-b775-566f232d68f8", + "exec": [ + "pm.test(\"View Group\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.name).to.eql(\"Main Services\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{api_key}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{endpoint}}/api/groups/1", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "groups", + "1" + ] + }, + "description": "View a specific service, this will include the service's failures and checkins." + }, + "response": [ + { + "name": "View Service", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{endpoint}}/api/services/1", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "services", + "1" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Mon, 10 Dec 2018 19:31:19 GMT" + }, + { + "key": "Content-Length", + "value": "482" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"name\": \"Google\",\n \"domain\": \"https://google.com\",\n \"expected\": null,\n \"expected_status\": 200,\n \"check_interval\": 10,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": null,\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 1,\n \"allow_notifications\": false,\n \"created_at\": \"2018-12-10T11:15:42.24769-08:00\",\n \"updated_at\": \"2018-12-10T11:15:42.247837-08:00\",\n \"online\": true,\n \"latency\": 0.190599816,\n \"ping_time\": 0.00476598,\n \"online_24_hours\": 0,\n \"avg_response\": \"\",\n \"status_code\": 200,\n \"last_success\": \"2018-12-10T11:31:13.511139-08:00\"\n}" + } + ] + }, + { + "name": "Create Group", + "event": [ + { + "listen": "test", + "script": { + "id": "d4eb16fe-8495-40e5-9ca3-be20951e5133", + "exec": [ + "pm.test(\"Create Group\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.output.name).to.eql(\"New Group\");", + " pm.globals.set(\"group_id\", jsonData.output.id);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{api_key}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Group\"\n}" + }, + "url": { + "raw": "{{endpoint}}/api/groups", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "groups" + ] + }, + "description": "Create a new service and begin monitoring." + }, + "response": [ + { + "name": "Create Service", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Service\",\n \"domain\": \"https://statping.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 30,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 30,\n \"order_id\": 0\n}" + }, + "url": { + "raw": "{{endpoint}}/api/services", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "services" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Mon, 10 Dec 2018 19:31:47 GMT" + }, + { + "key": "Content-Length", + "value": "528" + } + ], + "cookie": [], + "body": "{\n \"status\": \"success\",\n \"type\": \"service\",\n \"method\": \"create\",\n \"id\": 10,\n \"output\": {\n \"id\": 10,\n \"name\": \"New Service\",\n \"domain\": \"https://statping.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 30,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 30,\n \"order_id\": 0,\n \"allow_notifications\": false,\n \"created_at\": \"2018-12-10T11:31:47.535086-08:00\",\n \"updated_at\": \"2018-12-10T11:31:47.535184-08:00\",\n \"online\": false,\n \"latency\": 0,\n \"ping_time\": 0,\n \"online_24_hours\": 0,\n \"avg_response\": \"\",\n \"status_code\": 0,\n \"last_success\": \"0001-01-01T00:00:00Z\"\n }\n}" + } + ] + }, + { + "name": "Update Group", + "event": [ + { + "listen": "test", + "script": { + "id": "b5a67a19-fd08-40b0-a961-3e9474ab78c6", + "exec": [ + "pm.test(\"Update Service\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.output.name).to.eql(\"Updated Group\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{api_key}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Updated Group\"\n}" + }, + "url": { + "raw": "{{endpoint}}/api/groups/{{group_id}}", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "groups", + "{{group_id}}" + ] + }, + "description": "Update a service with new values and begin monitoring." + }, + "response": [ + { + "name": "Update Service", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Updated New Service\",\n \"domain\": \"https://google.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 60,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0\n}" + }, + "url": { + "raw": "{{endpoint}}/api/services/{{service_id}}", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "services", + "{{service_id}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Mon, 10 Dec 2018 19:31:54 GMT" + }, + { + "key": "Content-Length", + "value": "567" + } + ], + "cookie": [], + "body": "{\n \"status\": \"success\",\n \"type\": \"service\",\n \"method\": \"update\",\n \"id\": 10,\n \"output\": {\n \"id\": 10,\n \"name\": \"Updated New Service\",\n \"domain\": \"https://google.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 60,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0,\n \"allow_notifications\": false,\n \"created_at\": \"2018-12-10T11:31:47.535086-08:00\",\n \"updated_at\": \"2018-12-10T11:31:47.535184-08:00\",\n \"online\": true,\n \"latency\": 0.550636193,\n \"ping_time\": 0.073339805,\n \"online_24_hours\": 0,\n \"avg_response\": \"\",\n \"status_code\": 200,\n \"last_success\": \"2018-12-10T11:31:49.161389-08:00\"\n }\n}" + } + ] + }, + { + "name": "Delete Group", + "event": [ + { + "listen": "test", + "script": { + "id": "dd4d721d-d874-448b-abc9-59c1afceb58e", + "exec": [ + "pm.test(\"Delete Service\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.status).to.eql(\"success\");", + " pm.expect(jsonData.type).to.eql(\"group\");", + " pm.expect(jsonData.method).to.eql(\"delete\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{api_key}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{endpoint}}/api/groups/{{group_id}}", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "groups", + "{{group_id}}" + ] + }, + "description": "Delete a group" + }, + "response": [ + { + "name": "Delete Service", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{endpoint}}/api/services/{{service_id}}", + "host": [ + "{{endpoint}}" + ], + "path": [ + "api", + "services", + "{{service_id}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Mon, 10 Dec 2018 19:32:06 GMT" + }, + { + "key": "Content-Length", + "value": "567" + } + ], + "cookie": [], + "body": "{\n \"status\": \"success\",\n \"type\": \"service\",\n \"method\": \"delete\",\n \"id\": 10,\n \"output\": {\n \"id\": 10,\n \"name\": \"Updated New Service\",\n \"domain\": \"https://google.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 60,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0,\n \"allow_notifications\": false,\n \"created_at\": \"2018-12-10T11:31:47.535086-08:00\",\n \"updated_at\": \"2018-12-10T11:31:47.535184-08:00\",\n \"online\": true,\n \"latency\": 0.203382878,\n \"ping_time\": 0.001664491,\n \"online_24_hours\": 0,\n \"avg_response\": \"\",\n \"status_code\": 200,\n \"last_success\": \"2018-12-10T11:31:55.455091-08:00\"\n }\n}" + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{api_key}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "id": "4cd2ab82-e60d-45cd-9b74-cb4b5d893f4d", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "c7cb2b6d-289a-4073-b291-202bbec8cb44", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, { "name": "Users", "item": [ diff --git a/source/tmpl/services.gohtml b/source/tmpl/services.gohtml index 3d587071..2051d552 100644 --- a/source/tmpl/services.gohtml +++ b/source/tmpl/services.gohtml @@ -43,19 +43,17 @@ Name - Status {{range .Groups}} - + {{.Name}} - {{if .Online}}ONLINE{{else}}OFFLINE{{end}}
View - {{if Auth}}{{end}} + {{if Auth}}{{end}}
@@ -64,8 +62,8 @@ {{end}} {{if Auth}} -

Create Service

- {{template "form_service" NewService}} +

Create Group

+ {{template "form_group" NewGroup}} {{end}} diff --git a/source/wiki.go b/source/wiki.go index 40883468..0e5c5ba3 100644 --- a/source/wiki.go +++ b/source/wiki.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2018-12-31 03:39:26.710899 -0800 PST m=+0.479547222 +// 2018-12-31 13:16:43.415892 -0800 PST m=+1.140052566 // // This contains the most recently Markdown source for the Statping Wiki. package source