├── .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 | ![Google](/status/google.com-443.svg) 25 | ![Downtime repository](/status/github.com-lowlighter-downtime-443.svg) 26 | ![Google DNS](/status/8.8.8.8-53.svg) 27 | ![Unavailable website](/status/unavailable.website.com-443.svg) 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 | 2 | 60 | 61 |
<%= host.response_time.latest >= host.status_slow_ms ? "slow" : "" %>" xmlns="http://www.w3.org/1999/xhtml"> 62 | 63 | 88 |
89 |
90 |
-------------------------------------------------------------------------------- /templates/status.svg: -------------------------------------------------------------------------------- 1 | 2 | 73 | 74 |
<%= host.response_time.latest >= host.status_slow_ms ? "slow" : "" %>" xmlns="http://www.w3.org/1999/xhtml"> 75 | 76 | 122 |
123 |
124 |
-------------------------------------------------------------------------------- /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) => `![${host.title ?? host.name}](/${host.files.path.status})`), ""].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 | } --------------------------------------------------------------------------------