├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .yarn └── releases │ └── yarn-1.22.19.cjs ├── .yarnrc.yml ├── README.md ├── _routes.json ├── _worker.js ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── conf.example.json ├── core.example.php ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── main.js ├── router │ └── index.js └── views │ └── Home.vue ├── vue.config.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | VUE_APP_use_env = 1 # 此参数为 1 时启用环境变量代替conf.json,无法读取此参数时使用conf.json 2 | 3 | VUE_APP_config_title = 状态监控 # 页面标题 4 | VUE_APP_config_title_english = StatusLive #页面副标题 5 | 6 | VUE_APP_config_mode = 2 # 模式选项,1为公开模式,2为隐私模式 7 | VUE_APP_config_readonly_apikey = USE_SERVER_APIKEY # 公开模式用,填写从UptimeRobot后台获得的ReadOnly-ApiKey 8 | VUE_APP_config_proxy_link = /core.php # 隐私模式用,反代访问路径 9 | VUE_APP_config_history_time = 60 # 获取过去 X 天的可用率,单位为天 10 | VUE_APP_config_logs_history_days = 30 # 获取过去 X 天的状态日志,单位为天 11 | 12 | VUE_APP_config_success_min = 98 # 合格(success)等级标准,低于此数字为警告(warning)等级 13 | VUE_APP_config_warning_min = 90 # 警告(warning)等级标准,低于此数字为危险(danger)等级 14 | VUE_APP_config_auto_refresh_seconds = 60 # 自动刷新时间,单位为秒,填写0为禁用自动刷新 15 | 16 | VUE_APP_logs_each_page = 10 # 日志模块每页展示行数,v2.1新增,作用于日志查看区 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Status-Vercel 2 | 3 | on: 4 | push: 5 | branches: [ "master" , "freejishu-dev", "cloudflare-try" ] 6 | pull_request: 7 | branches: [ "master" , "freejishu-dev", "cloudflare-try" ] 8 | 9 | jobs: 10 | ci: 11 | name: Build & Test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'npm' 23 | env: 24 | CI: true 25 | - name: install yarn 26 | run: npm install yarn -g 27 | - name: yarn 28 | run: yarn 29 | - name: yarn build 30 | run: yarn build 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files & example file 7 | .env.local 8 | .env.*.local 9 | .dev.vars 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | config.json 26 | core.php -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-1.22.19.cjs 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StatusLive  Stars GitHub issues 2 | 3 |

简洁 · 快速 · 轻便

4 | 5 | ![qhn.webp](https://s2.loli.net/2022/10/31/N4sCi7YDIZ8XxST.webp) 6 | 7 | ## What's StatusLive? 8 | 9 | StatusLive 是一个基于 Uptimerobot 的状态页,数据基于 Uptimerobot API 而来,开箱即用。 10 | 11 | StatusLive is a status page based on Uptimerobot. The data is based on Uptimerobot API. 12 | 13 | 注册一个 Uptimerobot 账户并添加监测点,即可搭建属于自己的、高可用的状态页面。 14 | 15 | Register an Uptimerobot account and add monitoring points to build your own status page. 16 | 17 | ## Demo 18 | 19 | https://status.freejishu.com/ 20 | 21 | ## How to use 22 | 23 | - 最经典的打开方式 ( `dist` + `conf.json` + `core.php` ) : 24 | 25 | 1. 注册一个 `UptimeRobot` 账户并添加监控节点。 26 | 27 | 2. ⭐Star 一下,然后从 [Releases][1] 下载最新版本并解压。 28 | 29 | 3. 复制 `conf.example.conf` 到 `conf.json` ,并配置配置文件 [conf.json][2] 30 | ``` 31 | { 32 | "config_title": "状态监控", //页面标题 33 | "config_title_english": "StatusLive", //页面副标题 34 | 35 | "config_mode": 2, //模式选项,1为公开模式,2为隐私模式(模式区别请看下方) 36 | "config_readonly_apikey": "USE_SERVER_APIKEY", //公开模式用,填写从UptimeRobot后台获得的ReadOnly-ApiKey 37 | "config_proxy_link": "/core.php", //隐私模式用,反代访问路径 38 | 39 | "config_history_time": 60, //获取过去 X 天的可用率,单位为天 40 | "config_logs_history_days": 30, //获取过去 X 天的状态日志,单位为天 41 | 42 | "config_success_min": 98, //合格(success)等级标准,低于此数字为警告(warning)等级 43 | "config_warning_min": 90, //警告(warning)等级标准,低于此数字为危险(danger)等级 44 | "config_auto_refresh_seconds": 60, //自动刷新时间,单位为秒,填写0为禁用自动刷新 45 | 46 | "logs_each_page": 10 //日志模块每页展示行数,v2.1新增,作用于日志查看区 47 | } 48 | ``` 49 | - **公开模式(不推荐)** 50 | 51 | 最简单、快速的开箱方式。系统会根据 `conf.json` 内的 `config_readonly_apikey` 直接请求 UptimeRobot API 接口。此模式不需要 `core.php` 。 52 | 53 | `conf.json` 内应该如此填写: 54 | 55 | ``` 56 | "config_mode": 1, 57 | "config_readonly_apikey": "ur609xxx-27fxxxxxxxxxxxxxxxxxxxxx", 58 | ``` 59 | 60 | **注意:此模式下一定要使用 `只读ApiKey (Read-Only API Key)`,非只读ApiKey的泄露会导致其他人使用官方 API 操纵账户!** 61 | 62 | 如果觉得直接请求 UptimeRobot API 接口速度有些差,或不想暴露部分关键字段,您也可以使用下面的隐私模式反代以提高速度。 63 | 64 | - **隐私模式(推荐)** 65 | 66 | 由于UptimeRobot API返回数据内包含 `url` 、 `http_username` 、 `http_password` 、 `port` 等字段,直接请求可能会导致真实域名、IP等泄露;同时对免费账户,UptimeRobot 的 API 存在 QPS 限制。故推荐使用隐私模式,可隐去关键字段并针对性缓存。 67 | 68 | `conf.json` 内应该如此填写: 69 | 70 | ``` 71 | "config_mode": 2, 72 | "config_readonly_apikey": "USE_SERVER_APIKEY", //无需再填写apikey,以core.php中为准 73 | "config_proxy_link": "/core.php", //填写你的core.php路径 74 | ``` 75 | 76 | 本程序自带一个php的反代文件。对反代文件 `core.example.php` ,您需要先复制到 `core.php` (当然其他名字也可以,`config.json` 中的 `config_proxy_link` 字段需同步更新),再修改 `core.php` 的部分配置: 77 | 78 | ``` 79 | //在这里填入你的API_KEY,如果使用公开模式则置空避免key被更改。 80 | $apikey = 'ur609264-xxxxxxxxxxxxxxxxxxxxxxxx'; 81 | 82 | //json缓存文件名,可自行配置 83 | $file_name = 'uptime.json'; 84 | 85 | //缓存时间,单位为秒,因为UptimeRobotAPI免费用户调用限制为10次/分钟故不建议低于6 86 | $cache_time = 10; 87 | ``` 88 | 89 | 接下来您可以将其放到任意服务器上,只需做好 CORS 和在 `config_proxy_link` 中填写好地址即可。 90 | 91 | 关于反代机制,除了部署 `core.php` ,还有很多玩法,请参照下文中**更多打开方式**部分。 92 | 93 | - `conf.json` 的其余字段根据上文提示填写即可。注意JSON文件不能存在`注释`。 94 | 95 | 4. 上传到服务器,然后 Enjoy it! 96 | 97 |
98 | 99 | - 更多打开方式: 100 | 101 | 1. `conf.json` 可以被环境变量替代。若您部署到类似 Cloudflare Pages 或 Vercel 此类平台时,可通过填写环境变量生成 `.env` 文件。当检测到环境变量存在时,无需再修改或部署 `conf.json`,程序将根据环境变量启动。详见:[如何部署 StatusLive 到静态资源平台?](https://github.com/freejishu/StatusLive/discussions/30)。 102 | 103 | 2. 隐私模式需要用到的 `core.php` 可以有多种部署方式,如: 104 | - 可以使用 Cloudflare Worker 替代,详见:[使用 Cloudflare Workers 替代 Core.php 实现反代](https://github.com/freejishu/StatusLive/discussions/28)。 105 | - 如果计划将 `core.php` 用于如 `Vercel` 等 Serverless Functions 平台,请注释掉 `core.php` 的[第49行](https://github.com/freejishu/StatusLive/blob/67ebdce931332255f06dc0635aa0d88aa589999d/public/core.example.php#L49)避免写入错误。 106 | - 如果懒得架设 `core.php` ,可以使用由开发者提供的公共反代。请参照 [StatusLive公共反代使用说明](https://github.com/freejishu/StatusLive/discussions/15) 。 107 | - 关于 `core.php` 的更多细节,请参照 [常见问题汇总(v2.0)](https://github.com/freejishu/StatusLive/discussions/3)。 108 | 109 | 110 | ## Migration from v1.x 111 | 112 | v1.x 使用参照:https://www.freejishu.com/statuslive-for-you/ 113 | 114 | 基于各种各样的考虑,v2.x 采用了全新的技术栈,故 v1.x 不能直接迁移到 v2.x 。但是不用担心,基于开箱即用的特性,你会很快上手 v2.x 的。 115 | 116 | 注:v2.x 通过切换分支的形式实现过渡,即原 master 分支被重命名为 v1.x,而 v2.x 重命名为 master ,并切换了一次默认分支。如果之前 fork 过项目,可能需要重新 fork 或进行同步操作才能继续操作。 117 | 118 | ## Licenses 119 | 120 | MIT 121 | 122 | ## How to Rebuild 123 | 124 | 1. Clone the code 125 | 2. Project setup 126 | 127 | ``` 128 | yarn install 129 | ``` 130 | 131 | 3. Compiles and hot-reloads for development & Compiles and minifies for production 132 | 133 | ``` 134 | yarn serve 135 | yarn build 136 | ``` 137 | 138 | 4. Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). 139 | 140 | 141 | [1]: https://github.com/freejishu/StatusLive/releases/latest 142 | [2]: https://github.com/freejishu/StatusLive/blob/master/public/conf.json 143 | [3]: https://github.com/freejishu/StatusLive/discussions/3 144 | -------------------------------------------------------------------------------- /_routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "include": ["/api"], 4 | "exclude": [] 5 | } -------------------------------------------------------------------------------- /_worker.js: -------------------------------------------------------------------------------- 1 | let api_key = '';//你的apikey 2 | let cache_time = 10;//缓存时间(秒) 3 | const url = 'https://api.uptimerobot.com/v2/getMonitors'; 4 | 5 | export default { 6 | async fetch(request, env) { 7 | api_key = env.api_key ; 8 | cache_time = env.cache_time ? env.cache_time : 10; 9 | console.log(cache_time); 10 | const url = new URL(request.url); 11 | if (url.pathname.startsWith('/api')) { 12 | if (request.method === 'OPTIONS') { 13 | return new Response(null, { 14 | status: 200, 15 | statusText: 'OK', 16 | headers: { 17 | 'content-type': 'application/json;charset=UTF-8', 18 | 'Access-Control-Allow-Origin': '*', 19 | 'Access-Control-Allow-Methods': '*', 20 | 'Access-Control-Allow-Credentials': 'true', 21 | 'Access-Control-Allow-Headers': 'Content-Type', 22 | 'Access-Control-Expose-Headers': '*' 23 | } 24 | }) 25 | } else if (request.method === 'POST') { 26 | return await handleRequest(request,env); 27 | } else { 28 | return new Response(null, { 29 | status: 405, 30 | statusText: 'Method Not Allowed', 31 | }) 32 | } 33 | } 34 | // Otherwise, serve the static assets. 35 | // Without this, the Worker will error and no assets will be served. 36 | return env.ASSETS.fetch(request); 37 | }, 38 | } 39 | 40 | 41 | 42 | 43 | async function gatherResponse(response) { 44 | const { headers } = response; 45 | const contentType = headers.get('content-type') || ''; 46 | if (contentType.includes('application/json')) { 47 | return JSON.stringify(await response.json()); 48 | } 49 | return response.text(); 50 | } 51 | 52 | async function readRequestBody(request) { 53 | const { headers } = request; 54 | const contentType = headers.get('content-type') || ''; 55 | 56 | if (contentType.includes('application/json')) { 57 | return JSON.stringify(await request.json()); 58 | } else if (contentType.includes('application/text')) { 59 | return request.text(); 60 | } else if (contentType.includes('text/html')) { 61 | return request.text(); 62 | } else if (contentType.includes('form')) { 63 | const formData = await request.formData(); 64 | const body = {}; 65 | for (const entry of formData.entries()) { 66 | body[entry[0]] = entry[1]; 67 | } 68 | return JSON.stringify(body); 69 | } else { 70 | return 'a file'; 71 | } 72 | } 73 | 74 | async function handleRequest(request,env) { 75 | 76 | const timestamp = (Date.parse(new Date()))/1000; 77 | const lasttime = await env.statuslive.get("statuslive_lasttime"); 78 | let results = ''; 79 | let cache_tag = ''; 80 | if (timestamp - lasttime <= cache_time) { 81 | results = await env.statuslive.get("statuslive_json_cache"); 82 | cache_tag = 'Cache Hit - Time:'+lasttime; 83 | }else{ 84 | const post_data = JSON.parse(await readRequestBody(request)); 85 | post_data.api_key = api_key; 86 | 87 | const init = { 88 | headers: { 89 | 'content-type': 'application/json;charset=UTF-8', 90 | }, 91 | method: 'POST', 92 | body: await JSON.stringify(post_data) 93 | }; 94 | const response = await fetch(url, init); 95 | results = await JSON.parse(await gatherResponse(response)); 96 | 97 | for (let index = 0; index < results.monitors.length; index++) { 98 | results.monitors[index].url = ""; 99 | results.monitors[index].http_username = ""; 100 | results.monitors[index].http_password = ""; 101 | results.monitors[index].port = ""; 102 | } 103 | 104 | results = await JSON.stringify(results); 105 | await env.statuslive.put("statuslive_json_cache", results, {expirationTtl: (cache_time < 60 ? 60 : cache_time)}) 106 | await env.statuslive.put("statuslive_lasttime", timestamp.toString(), {expirationTtl: (cache_time < 60 ? 60 : cache_time)}) 107 | cache_tag = 'Cache Miss - Time:'+timestamp; 108 | } 109 | 110 | let response_init = { 111 | headers: { 112 | 'content-type': 'application/json;charset=UTF-8', 113 | 'Access-Control-Allow-Origin': '*', 114 | 'Access-Control-Allow-Methods': '*', 115 | 'Access-Control-Allow-Credentials': 'true', 116 | 'Access-Control-Allow-Headers': 'Content-Type', 117 | 'Access-Control-Expose-Headers': '*', 118 | 'StatusLive-Cache-Tag' : cache_tag 119 | } 120 | } 121 | return new Response(results, response_init); 122 | } 123 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statuslive_vue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.4", 12 | "core-js": "^3.6.5", 13 | "echarts": "^5.4.0", 14 | "element-ui": "^2.15.6", 15 | "v-charts-v2": "^2.0.9", 16 | "vue": "^2.6.11", 17 | "vue-echarts": "^6.2.3", 18 | "vue-router": "^3.2.0" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~4.5.0", 22 | "@vue/cli-plugin-eslint": "~4.5.0", 23 | "@vue/cli-plugin-router": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "@vue/composition-api": "^1.7.1", 26 | "babel-eslint": "^10.1.0", 27 | "eslint": "^6.7.2", 28 | "eslint-plugin-vue": "^6.2.2", 29 | "vue-template-compiler": "^2.6.11" 30 | }, 31 | "eslintConfig": { 32 | "root": true, 33 | "env": { 34 | "node": true 35 | }, 36 | "extends": [ 37 | "plugin:vue/essential", 38 | "eslint:recommended" 39 | ], 40 | "parserOptions": { 41 | "parser": "babel-eslint" 42 | }, 43 | "rules": {} 44 | }, 45 | "browserslist": [ 46 | "> 1%", 47 | "last 2 versions", 48 | "not dead" 49 | ], 50 | "engines": { 51 | "yarn": "=1.22.19", 52 | "node": "=16" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/conf.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "config_title": "状态监控", 3 | "config_title_english": "StatusLive", 4 | 5 | "config_mode": 2, 6 | "config_readonly_apikey": "USE_SERVER_APIKEY", 7 | "config_proxy_link": "/core.php", 8 | "config_history_time": 60, 9 | "config_logs_history_days": 30, 10 | 11 | "config_success_min": 98, 12 | "config_warning_min": 90, 13 | "config_auto_refresh_seconds": 60, 14 | 15 | "logs_each_page": 10 16 | } -------------------------------------------------------------------------------- /public/core.example.php: -------------------------------------------------------------------------------- 1 | api_key = $apikey; 26 | } 27 | $data = http_build_query($data); 28 | $curl = curl_init(); 29 | curl_setopt($curl, CURLOPT_URL, $link); 30 | curl_setopt($curl, CURLOPT_POST, 1); 31 | curl_setopt($curl, CURLOPT_POSTFIELDS, $data); 32 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 33 | $output = curl_exec($curl); 34 | curl_close($curl); 35 | $o = json_decode($output, true); 36 | if(@$o['stat']=='ok'){ 37 | header("StatusLiveCache: Miss."); 38 | //替换删除敏感数据,如有其他需要可以自行增加 39 | for ($i = 0; $i < count($o['monitors']); $i++) { 40 | $o['monitors'][$i]['url'] = ""; 41 | $o['monitors'][$i]['http_username'] = ""; 42 | $o['monitors'][$i]['http_password'] = ""; 43 | $o['monitors'][$i]['port'] = ""; 44 | } 45 | $json = []; 46 | $json['time'] = time(); 47 | $json['json'] = json_encode($o); 48 | 49 | file_put_contents($file_name, json_encode($json)); 50 | 51 | exit($json['json']); 52 | }else{ 53 | //请求错误不缓存 54 | header("StatusLiveCache: Miss, Request Fail."); 55 | exit(json_encode($o)); 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freejishu/StatusLive/37d992868bb65ce564e197fab0459a12e7d643a2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freejishu/StatusLive/37d992868bb65ce564e197fab0459a12e7d643a2/src/assets/logo.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import ElementUI from 'element-ui'; 3 | import 'element-ui/lib/theme-chalk/index.css'; 4 | import 'element-ui/lib/theme-chalk/display.css'; 5 | import axios from 'axios'; 6 | 7 | import App from './App.vue'; 8 | import router from './router'; 9 | 10 | 11 | Vue.config.productionTip = false; 12 | 13 | Vue.prototype.$axios = axios; 14 | //axios.defaults.baseURL = '/v2'; 15 | 16 | 17 | 18 | Vue.use(ElementUI); 19 | router.beforeEach((to, from, next) => { 20 | if(to.meta.title){ 21 | document.title = to.meta.title 22 | } 23 | next(); 24 | }) 25 | 26 | new Vue({ 27 | router, 28 | render: h => h(App) 29 | }).$mount('#app') 30 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: Home, 12 | meta:{ 13 | title: 'StatusLive' 14 | } 15 | } 16 | ] 17 | 18 | const router = new VueRouter({ 19 | routes 20 | }) 21 | 22 | export default router 23 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 202 | 203 | 325 | 326 | 851 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | disableHostCheck: true 4 | } 5 | } --------------------------------------------------------------------------------