├── .gitignore
├── CNAME
├── static
├── site
├── styles.css
└── script.js
├── .gitattributes
├── hosts
├── list
├── 8.8.8.8-53
├── google.com-443
├── unavailable.website.com-443
└── github.com-lowlighter-downtime-443
├── index.html
├── source
├── server.ts
└── tests.ts
├── README.md
├── .github
└── workflows
│ └── connection_tests.yml
├── LICENSE
├── config.yml
├── status
├── 8.8.8.8-53-badge.svg
├── unavailable.website.com-443-badge.svg
├── 8.8.8.8-53.svg
├── unavailable.website.com-443.svg
├── google.com-443-badge.svg
├── google.com-443.svg
├── github.com-lowlighter-downtime-443-badge.svg
└── github.com-lowlighter-downtime-443.svg
├── USAGE.md
└── templates
├── badge.svg
└── status.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | downtime.lecoq.io
--------------------------------------------------------------------------------
/static/site:
--------------------------------------------------------------------------------
1 | {"title":"Downtime","favicon":"https://simon.lecoq.io/src/icon.png","refresh_rate_sec":120}
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Linguist ignores
5 | static/site linguist-generated
6 | hosts/* linguist-generated
7 | status/* linguist-generated
--------------------------------------------------------------------------------
/hosts/list:
--------------------------------------------------------------------------------
1 | {"hosts":[{"name":"google.com","status":"status/google.com-443.svg"},{"name":"github.com/lowlighter/downtime","status":"status/github.com-lowlighter-downtime-443.svg"},{"name":"8.8.8.8","status":"status/8.8.8.8-53.svg"},{"name":"unavailable.website.com","status":"status/unavailable.website.com-443.svg"}]}
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Downtime
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/source/server.ts:
--------------------------------------------------------------------------------
1 | //Import
2 | import * as fs from "https://deno.land/std@0.80.0/fs/mod.ts"
3 | import * as Server from "https://deno.land/std@0.80.0/http/server.ts"
4 |
5 | //Serve requests
6 | const server = Server.serve({hostname:"0.0.0.0", port:4000})
7 | for await (const request of server) {
8 | const path = request.url.replace(/^[/]/, "").replace(/[?].*$/, "").replace(/^$/, "index.html")
9 | if (await fs.exists(path))
10 | request.respond({status:200, body:await Deno.readTextFile(path)})
11 | else
12 | request.respond({status:404})
13 | }
--------------------------------------------------------------------------------
/hosts/8.8.8.8-53:
--------------------------------------------------------------------------------
1 | {"port":53,"status_slow_ms":30000,"name":"8.8.8.8","created":"2020-12-12T00:02:13.915Z","files":{"filename":"8.8.8.8-53","path":{"hosts":"hosts/8.8.8.8-53","status":"status/8.8.8.8-53.svg","badges":"status/8.8.8.8-53-badge.svg"}},"title":"Google DNS","use":"ncat","updated":"2021-05-21T19:06:34.971Z","tests":5,"uptime":{"tests":[{"t":"2021-05-21T18:52:44.259Z","v":0},{"t":"2021-05-21T19:01:22.526Z","v":0},{"t":"2021-05-21T19:04:44.788Z","v":0},{"t":"2021-05-21T19:06:34.971Z","v":0}],"days":[{"t":"2020-12-12","v":1}],"overall":0.2,"last24h":0,"latest":0},"response_time":{"tests":[{"t":"2020-12-12T00:02:13.947Z","v":10}],"days":[],"overall":10,"last24h":10,"latest":10}}
--------------------------------------------------------------------------------
/hosts/google.com-443:
--------------------------------------------------------------------------------
1 | {"port":443,"status_slow_ms":30000,"name":"google.com","created":"2020-12-12T00:02:13.915Z","files":{"filename":"google.com-443","path":{"hosts":"hosts/google.com-443","status":"status/google.com-443.svg","badges":"status/google.com-443-badge.svg"}},"title":"Google","updated":"2021-05-21T19:06:34.981Z","tests":5,"uptime":{"tests":[{"t":"2021-05-21T18:52:44.281Z","v":1},{"t":"2021-05-21T19:01:22.553Z","v":1},{"t":"2021-05-21T19:04:44.814Z","v":1},{"t":"2021-05-21T19:06:34.981Z","v":1}],"days":[{"t":"2020-12-12","v":1}],"overall":1,"last24h":1,"latest":1},"response_time":{"tests":[{"t":"2021-05-21T18:52:44.281Z","v":14.144},{"t":"2021-05-21T19:01:22.553Z","v":8.286},{"t":"2021-05-21T19:04:44.814Z","v":6.921},{"t":"2021-05-21T19:06:34.981Z","v":5.368}],"days":[{"t":"2020-12-12","v":9.575}],"overall":8.8588,"last24h":8.67975,"latest":5.368}}
--------------------------------------------------------------------------------
/hosts/unavailable.website.com-443:
--------------------------------------------------------------------------------
1 | {"port":443,"status_slow_ms":30000,"name":"unavailable.website.com","created":"2020-12-12T00:02:13.915Z","files":{"filename":"unavailable.website.com-443","path":{"hosts":"hosts/unavailable.website.com-443","status":"status/unavailable.website.com-443.svg","badges":"status/unavailable.website.com-443-badge.svg"}},"title":"Unavailable website","updated":"2021-05-21T19:06:35.245Z","tests":5,"uptime":{"tests":[{"t":"2021-05-21T18:52:44.476Z","v":0},{"t":"2021-05-21T19:01:22.699Z","v":0},{"t":"2021-05-21T19:04:45.012Z","v":0},{"t":"2021-05-21T19:06:35.245Z","v":0}],"days":[{"t":"2020-12-12","v":0}],"overall":0,"last24h":0,"latest":0},"response_time":{"tests":[{"t":"2021-05-21T18:52:44.476Z","v":0},{"t":"2021-05-21T19:01:22.699Z","v":0},{"t":"2021-05-21T19:04:45.012Z","v":0},{"t":"2021-05-21T19:06:35.245Z","v":0}],"days":[{"t":"2020-12-12","v":0}],"overall":0,"last24h":0,"latest":0}}
--------------------------------------------------------------------------------
/static/styles.css:
--------------------------------------------------------------------------------
1 | body{
2 | font-family: lato, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | margin: 0;
8 | padding: 0 1rem;
9 | background-color: #06090F;
10 | color: #C9D1D9;
11 | }
12 | @media (prefers-color-scheme: light) {
13 | body {
14 | background: white;
15 | color: black;
16 | }
17 | }
18 | nav {
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 | width: 100%;
23 | height: 4rem;
24 | }
25 | nav .name {
26 | max-width: 864px;
27 | width: 100%;
28 | font-weight: bold;
29 | }
30 | main {
31 | display: flex;
32 | flex-wrap: wrap;
33 | max-width: 864px;
34 | width: 100%;
35 | }
36 | main img {
37 | margin: 4px 16px;
38 | width: 400px;
39 | max-width: 100%;
40 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 💹 Downtime
2 |
3 | ## 💬 How to use ?
4 |
5 | 0. Fork this repository and enable GitHub actions on it
6 | 1. Edit [config.yml](/config.yml) to add your hosts and settings
7 | 2. (optional) Go to repository `settings` and enable GitHub pages
8 |
9 | You're done !
10 |
11 | ## ✨ Features
12 |
13 | * No additional token needed, no security risks yay 🎉
14 | * Status badges will auto-update auto-magically without committing again
15 | * History stored as JSON for easy reuse as API
16 | * Easily deployed, easily tweakable
17 | * Supports `curl`, `ncat` and `telnet`
18 |
19 | See [USAGE.md](/USAGE.md) for more informations.
20 |
21 | ## 🚥 Current status
22 |
23 |
24 | 
25 | 
26 | 
27 | 
28 |
29 |
--------------------------------------------------------------------------------
/hosts/github.com-lowlighter-downtime-443:
--------------------------------------------------------------------------------
1 | {"port":443,"status_slow_ms":30000,"name":"github.com/lowlighter/downtime","created":"2020-12-12T00:02:13.915Z","files":{"filename":"github.com-lowlighter-downtime-443","path":{"hosts":"hosts/github.com-lowlighter-downtime-443","status":"status/github.com-lowlighter-downtime-443.svg","badges":"status/github.com-lowlighter-downtime-443-badge.svg"}},"title":"Downtime repository","updated":"2021-05-21T19:06:35.025Z","tests":5,"uptime":{"tests":[{"t":"2021-05-21T18:52:44.327Z","v":1},{"t":"2021-05-21T19:01:22.610Z","v":1},{"t":"2021-05-21T19:04:44.847Z","v":1},{"t":"2021-05-21T19:06:35.025Z","v":1}],"days":[{"t":"2020-12-12","v":1}],"overall":1,"last24h":1,"latest":1},"response_time":{"tests":[{"t":"2021-05-21T18:52:44.327Z","v":14.943},{"t":"2021-05-21T19:01:22.610Z","v":16.281},{"t":"2021-05-21T19:04:44.847Z","v":14.607},{"t":"2021-05-21T19:06:35.025Z","v":8.534}],"days":[{"t":"2020-12-12","v":19.198}],"overall":14.7126,"last24h":13.591249999999999,"latest":8.534}}
--------------------------------------------------------------------------------
/.github/workflows/connection_tests.yml:
--------------------------------------------------------------------------------
1 | name: Connection tests
2 |
3 | on:
4 | schedule:
5 | - cron: '*/5 * * * *'
6 | push:
7 | branches:
8 | - master
9 | - main
10 |
11 | jobs:
12 | connection-tests:
13 | runs-on: ubuntu-latest
14 | steps:
15 |
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Setup
22 | uses: denolib/setup-deno@v2
23 |
24 | - name: Additional setup
25 | run: |
26 | sudo apt-get install nmap
27 | sudo npm -g install svgo
28 | git config --local user.email "null@github.com"
29 | git config --local user.name "GitHub Action"
30 |
31 | - name: Perform connection tests
32 | run: deno run --allow-net --allow-read --allow-write --allow-run --unstable source/tests.ts
33 |
34 | - name: Optimize and commit results
35 | run: |
36 | npx svgo --multipass -f status
37 | git add static/site hosts status README.md
38 | git commit -m "Update connection status"
39 | git push
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 lowlighter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config.yml:
--------------------------------------------------------------------------------
1 | # Host list
2 | # "name" is mandatory and will be used as target to connect
3 | # "title" will be displayed as "human name" instead of "name" if specified
4 | # All defaults values can be overriden per host basis
5 | hosts:
6 | # Example / test root domain
7 | - name: google.com
8 | title: Google
9 | # Example / test url
10 | - name: github.com/lowlighter/downtime
11 | title: Downtime repository
12 | # Example / test ip with port using ncat
13 | - name: "8.8.8.8"
14 | title: Google DNS
15 | port: 53
16 | use: ncat
17 | # Example / unavailable website
18 | - name: unavailable.website.com
19 | title: Unavailable website
20 |
21 | # Site configuration
22 | site:
23 | # Title
24 | title: Downtime
25 | # Favicon / logo
26 | favicon: https://simon.lecoq.io/src/icon.png
27 | # Refresh rate of status images for client
28 | refresh_rate_sec: 120
29 |
30 | # Default values
31 | defaults:
32 | # Default command to use
33 | # Supported values are :
34 | # - "curl" [supports both uptime and response time, but only for HTTP/S requests]
35 | # - "ncat" [supports both uptime and response time]
36 | # - "telnet" [supports only uptime]
37 | use: curl
38 | # Default timeout
39 | timeout: 30
40 | # Default port
41 | port: 443
42 | # Default number of ms before considering that host have latency problems
43 | status_slow_ms: 30000
--------------------------------------------------------------------------------
/static/script.js:
--------------------------------------------------------------------------------
1 | (async function () {
2 | const config = await fetch("static/site").then(response => response.json())
3 | //Update title
4 | document.title = config.title ?? "Downtime"
5 | document.querySelector(".name").innerText = document.title
6 | //Update logo/favicon
7 | if (config.favicon) {
8 | //Update logo
9 | const img = document.createElement("img")
10 | img.src = config.favicon
11 | //Update favicon
12 | let favicon = document.querySelector("link[rel~='icon']")
13 | if (!favicon) {
14 | favicon = document.createElement("link")
15 | favicon.rel = "icon"
16 | document.querySelector("head").appendChild(favicon)
17 | }
18 | favicon.href = config.favicon
19 | }
20 | //Update
21 | ;(async function update() {
22 | try {
23 | const {hosts} = await fetch("hosts/list").then(response => response.json())
24 | for (const {name, status} of hosts) {
25 | let img = document.querySelector(`img[data-status='${status}']`)
26 | if (!img) {
27 | img = document.createElement("img")
28 | img.dataset.status = status
29 | img.alt = name
30 | document.querySelector("main").append(img)
31 | }
32 | img.src = `${status}?t=${Date.now()}`
33 | }
34 | } catch {} finally {
35 | setTimeout(update, (config.refresh_rate_sec ?? 2*60)*1000)
36 | }
37 | })()
38 | })()
--------------------------------------------------------------------------------
/status/8.8.8.8-53-badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/status/unavailable.website.com-443-badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/status/8.8.8.8-53.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/status/unavailable.website.com-443.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/USAGE.md:
--------------------------------------------------------------------------------
1 | # 💹 Usage
2 |
3 | ## 💬 How to use ?
4 |
5 | 0. Fork this repository
6 | 1. Edit [config.yml](config.yml) to add your hosts and settings.
7 | 2. (optional) Go to repository `settings` and enable GitHub pages.
8 |
9 | You're done !
10 |
11 | Connections tests are stored in [hosts](hosts) while generated SVG images are available in [status](status) folder.
12 | You can embed these images anywhere you want, and they'll be updated auto-magically !
13 |
14 | Repository can be private and it'll still works if you enabled GitHub pages.
15 |
16 | ## 🧰 How it works ?
17 |
18 | Each 5 minutes (or upon push), the runner will load your `config.yml`.
19 |
20 | For each specified hosts, using either `curl`, `ncat` or `telnet`, it will perform a connection test.
21 | Results are saved in [hosts](hosts), compacted each day, and dismissed after 48h.
22 | SVG images that you can embed everywhere for status will be generated in [status](status).
23 |
24 | These are commited with the default `GITHUB_TOKEN` so you don't need to create a personal token.
25 |
26 | ## 🗃️ Structure
27 |
28 | * `└── config.yml` contains action configuration
29 |
30 | * `└── .github` contains GitHub related files
31 | * `└── workflows` contains GitHub action workflows
32 | * `└── connection_tests.yml.yml` contains source code of the connection tests
33 |
34 | * `└── source` contains source code
35 | * `├── tests.ts` contains the connection tests source code
36 | * To run locally, use `deno run --allow-net --allow-read --allow-write --allow-run --unstable source/tests.ts`
37 | * `└── server.ts` contains a local server which can be used for development
38 | * To run locally, use `deno run --allow-net=0.0.0.0 --allow-read --unstable source/server.ts`
39 |
40 | * `├── index.html` contains site entry point
41 | * `└── static` contains site static assets
42 | * `├── script.js` contains executed JavaScript by client
43 | * `├── styles.css` contains CSS stylesheet applied to client
44 | * `└── site` contains site config data as JSON `⚙️`
45 |
46 | * `├── status` contains SVG images displaying the status for each host/endpoint `⚙️`
47 | * `└── hosts` contains connection tests results for each host/endpoint as JSON `⚙️`
48 | * `└── list` contains the list of hosts as JSON used by client `⚙️`
49 |
50 | * `└── templates` contains templates files
51 | * `└── status.svg` contains the SVG template used to generate SVG images
52 |
53 | Files marked with `⚙️` are auto-generated.
54 |
--------------------------------------------------------------------------------
/templates/badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/status.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/status/google.com-443-badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/status/google.com-443.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/status/github.com-lowlighter-downtime-443-badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/status/github.com-lowlighter-downtime-443.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/source/tests.ts:
--------------------------------------------------------------------------------
1 | //Imports
2 | import { ensureDir, exists, walkSync } from "https://deno.land/std@0.97.0/fs/mod.ts"
3 | import { readAll } from "https://deno.land/std@0.97.0/io/mod.ts"
4 | import * as YAML from "https://deno.land/std@0.97.0/encoding/yaml.ts"
5 | import * as ejs from "https://deno.land/x/dejs@0.9.3/mod.ts"
6 | export default {}
7 |
8 | //Types
9 | type config = any
10 | type host = any
11 |
12 | //Initialization
13 | const config = YAML.parse(await Deno.readTextFile("config.yml")) as config
14 | const debug = (left:string, right:string|null = null) => console.debug(`${(right ? left : "*").padEnd(24)} | ${(right ?? left).replace(/\n/g, "\\n").trim()}`)
15 | await Promise.all(["hosts", "status"].map(directory => ensureDir(directory)))
16 |
17 | //Test hosts
18 | const hosts = await Promise.all(config.hosts.map(async ({name, port = config.defaults?.port ?? 443, ...properties}:host) => {
19 |
20 | //Prepare hostname, filename and paths
21 | const hostname = `${name}:${port}`
22 | const filename = encodeURIComponent(hostname).replace(/%[0-9A-F]{2}/gi, "-")
23 | const path = {hosts:`hosts/${filename}`, status:`status/${filename}.svg`, badges:`status/${filename}-badge.svg`}
24 |
25 | //Prepare host data
26 | const data = {name, created:new Date()} as host
27 | //Reload from file
28 | if (await exists(path.hosts)) {
29 | Object.assign(data, JSON.parse(await Deno.readTextFile(path.hosts)))
30 | debug(`loaded ${path.hosts}`)
31 | }
32 | //Merge with properties
33 | Object.assign(data, properties)
34 |
35 | //Perform connection test
36 | const {use = config.defaults?.use ?? "", timeout = config.defaults?.timeout ?? 30} = data
37 | debug(hostname, `loaded`)
38 | //Select command to use
39 | const command =
40 | use === "curl" ? `curl -o /dev/null -m ${timeout} -Lsw 'received in %{time_connect} seconds\n' ${hostname}` :
41 | use === "ncat" ? `ncat -zvw${timeout} ${name} ${port}` :
42 | `echo -e '\x1dclose\x0d' | telnet ${name} ${443}`
43 | debug(hostname, `${command}`)
44 | //Execute command
45 | const test = Deno.run({cmd:["bash", "-c", command], stdout:"piped", stderr:"piped", stdin:"null"})
46 | const stdio = (await Promise.all([test.stdout, test.stderr].map(async stdio => new TextDecoder().decode(await readAll(stdio))))).join("\n")
47 | const {success, code} = await test.status()
48 | const latency = Number(stdio.match(/received in (?[0-9.]+) seconds/m)?.groups?.latency)*1000
49 | debug(hostname, `exited with code ${code} (${success ? "success" : "failed"} - latency ${latency} ms)`)
50 | //Patch status for curl
51 | let status = +success
52 | if ((!status)&&(use === "curl")&&(code === 52)) {
53 | status = 1
54 | debug(hostname, `empty curl result, but server is up (code 52)`)
55 | }
56 |
57 | //Compute results
58 | const {updated:_last_updated = new Date(), uptime = {tests:[], days:[], overall:NaN, last24h:NaN, latest:NaN}, response_time = {tests:[], days:[], overall:NaN, last24h:NaN, latest:NaN}, tests = 0, status_slow_ms = config.defaults?.status_slow_ms ?? 30*1000} = data
59 | const last_updated = new Date(_last_updated)
60 | const updated = new Date()
61 | for (const {logname, categorie, value} of [
62 | {logname:"uptime", categorie:uptime, value:status},
63 | {logname:"response_time", categorie:response_time, value:latency},
64 | ]) {
65 | //Skip invalid values
66 | if (Number.isNaN(value))
67 | continue
68 | //Save last value
69 | categorie.latest = value
70 | categorie.tests.push({t:updated, v:value})
71 | //Save overall value
72 | categorie.overall = Number.isNaN(categorie.overall) ? value : (value+categorie.overall*tests)/(tests+1)
73 | debug(hostname, `last ${logname} is ${value} (overall ${categorie.overall})`)
74 | //Save average value over 24 hours
75 | {
76 | //Compute average
77 | const period = new Date(updated)
78 | period.setHours(-24)
79 | const filtered = categorie.tests.filter(({t}:{t:Date}) => new Date(t) > period)
80 | const average = filtered.reduce((sum:number, {v:value}:{v:number}) => sum+value, 0)/filtered.length
81 | //Save average
82 | categorie.last24h = average
83 | debug(hostname, `last 24h ${logname} is ${average}`)
84 | }
85 | //Compact values of previous day on new day
86 | if (last_updated.getDay() !== updated.getDay()) {
87 | //Compute average
88 | const yesterday = last_updated.toISOString().substring(0, 10)
89 | debug(hostname, `compacting ${logname} previous day ${yesterday}`)
90 | const filtered = categorie.tests.filter(({t}:{t:Date}) => new Date(t).getDay() === last_updated.getDay())
91 | const average = filtered.reduce((sum:number, {v:value}:{v:number}) => sum+value, 0)/filtered.length
92 | //Save average
93 | categorie.days.push({t:yesterday, v:average})
94 | debug(hostname, `comptacted previous day to ${average} (${filtered.length} values)`)
95 | }
96 | //Filter values older than 48h
97 | {
98 | //Filter values
99 | const period = new Date(updated)
100 | period.setHours(-48)
101 | const filtered = categorie.tests.filter(({t}:{t:Date}) => new Date(t) > period)
102 | if (filtered.length < categorie.tests.length) {
103 | debug(hostname, `filtered ${categorie.tests.length - filtered.length} values of ${name}`)
104 | categorie.tests = filtered
105 | }
106 | }
107 | }
108 | //Save result to file
109 | const result = {port, status_slow_ms,
110 | ...data, updated, tests:tests+1, uptime, response_time, files:{filename, path},
111 | }
112 | delete result.icon
113 | await Deno.writeTextFile(path.hosts, JSON.stringify(result))
114 |
115 | //Extract domain and favicon
116 | let domain = name
117 | try { domain = new URL(`https://${name}`).hostname } catch {
118 | try { domain = new URL(name).hostname } catch {}
119 | }
120 | debug(hostname, `domain is ${domain}`)
121 | let favicon = null
122 | try {
123 | favicon = await fetch(`https://favicongrabber.com/api/grab/${domain}`).then(response => response.json()).then(({icons}) => icons.filter(({src = ""}) => /[.]ico$/.test(src)).shift()?.src) ?? null
124 | debug(hostname, `fetching favicon from ${favicon}`)
125 | } catch {
126 | debug(hostname, `no favicon found`)
127 | }
128 | //Load favicon as base64
129 | const icon = favicon ? await fetch(favicon).then(response => response.blob()).then(blob => new Promise((solve, reject) => {
130 | const reader = new FileReader()
131 | reader.onerror = reject
132 | reader.onload = () => solve(reader.result)
133 | reader.readAsDataURL(blob)
134 | })) : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABs0lEQVR4AWL4//8/RRjO8Iucx+noO0O2qmlbUEnt5r3Juas+hsQD6KaG7dqCKPgx72Pe9GIY27btZBrbtm3btm0nO12D7tVXe63jqtqqU/iDw9K58sEruKkngH0DBljOE+T/qqx/Ln718RZOFasxyd3XRbWzlFMxRbgOTx9QWFzHtZlD+aqLb108sOAIAai6+NbHW7lUHaZkDFJt+wp1DG7R1d0b7Z88EOL08oXwjokcOvvUxYMjBFCamWP5KjKBjKOpZx2HEPj+Ieod26U+dpg6lK2CIwTQH0oECGT5eHj+IgSueJ5fPaPg6PZrz6DGHiGAISE7QPrIvIKVrSvCe2DNHSsehIDatOBna/+OEOgTQE6WAy1AAFiVcf6PhgCGxEvlA9QngLlAQCkLsNWhBZIDz/zg4ggmjHfYxoPGEMPZECW+zjwmFk6Ih194y7VHYGOPvEYlTAJlQwI4MEhgTOzZGiNalRpGgsOYFw5lEfTKybgfBtmuTNdI3MrOTAQmYf/DNcAwDeycVjROgZFt18gMso6V5Z8JpcEk2LPKpOAH0/4bKMCAYnuqm7cHOGHJTBRhAEJN9d/t5zCxAAAAAElFTkSuQmCC"
135 |
136 | //Generate status SVG
137 | await Deno.writeTextFile(path.status, await ejs.renderFileToString("templates/status.svg", {host:{...result, icon}}))
138 | debug(hostname, `generated ${path.status}`)
139 |
140 | //Generate status badge SVG
141 | await Deno.writeTextFile(path.badges, await ejs.renderFileToString("templates/badge.svg", {host:{...result, icon}}))
142 | debug(hostname, `generated ${path.badges}`)
143 |
144 | //Return result
145 | return result as host
146 | }))
147 |
148 | //Updates
149 | {
150 | //Update hosts list
151 | await Deno.writeTextFile("hosts/list", JSON.stringify({hosts:hosts.map(({name, files}:host) => ({name, status:files.path.status}))}))
152 | debug(`updated hosts/list`)
153 |
154 | //Update site config
155 | await Deno.writeTextFile("static/site", JSON.stringify({...config.site}))
156 | debug(`updated static/site`)
157 |
158 | //Update readme
159 | await Deno.writeTextFile("README.md", (await Deno.readTextFile("README.md"))
160 | .replace(/[\s\S]*?/g,
161 | ["", ...hosts.map((host:host) => ``), ""].join("\n"))
162 | )
163 | debug(`updated README.md`)
164 | }
165 |
166 | //Cleans
167 | {
168 | //Clean generated files among hosts and status
169 | for (const directory of ["hosts", "status"] as const) {
170 | //List files to keep
171 | const keeping = [directory, ...hosts.map((host:host) => host.files.path[directory]), ...{hosts:["hosts/list"], status:[...hosts.map((host:host) => host.files.path.badges)]}[directory]]
172 | //Iterate through directory
173 | for (const file of walkSync(directory)) {
174 | //Clean residual files
175 | if (!keeping.includes(file.path.replace(/[/\\]/g, "/"))) {
176 | await Deno.remove(file.path)
177 | debug(`cleaned residual ${file.path}`)
178 | }
179 | }
180 | }
181 | }
--------------------------------------------------------------------------------