# shiroko-monitor
轻量级服务监控面板。借鉴 Uptime Kuma 的交互思路,代码完全自研,**零运行时依赖**(Node ≥ 18 直接跑,无 npm install),纯静态前端 + 可插拔探针,支持本地 / GitHub Pages / Cloudflare Workers / EdgeOne Pages 等多种低成本部署。
## 功能特性
- **HTTP 可用性探测**:状态码(支持自定义期望值,如把 500 判为正常)、响应延迟、超时控制、不自动跟随重定向(保留 3xx 原始码)
- **TCP 端口探测**:连通性判定、连接耗时,区分「连接被拒绝」与「超时」
- **Banner 探测(SSH / SMTP / FTP / POP3 / IMAP 等)**:连接后**被动读取**服务端主动发送的标识串,支持 `expectBanner` 规则校验(可做版本合规检查);不做认证、不发任何数据,不会触发目标侧的 fail2ban 封禁。默认只输出协议族摘要,不回显版本号
- **90 天可用率**:按天聚合每次探测结果(保留 90 天,自动淘汰旧数据),卡片底部以 90 格色条展示每日可用率(≥99% 绿 / ≥95% 橙 / <95% 红 / 无数据灰),悬停显示当日「可用率(在线次数/总次数)」;顶部汇总整体可用率
- **两种面板形态**(顶部一键切换,也可用 `settings.view` / `?view=` 指定):
- **表格**:一行一服务的高密度列表(状态/名称/目标/延迟/结果/最近检测/心率/可用率),默认视图
- **卡片**:一服务一卡,含心率图与可用率条
- **数据源过滤**:`settings.dataFilter` 可选 `both`(默认,两边都展示)/ `config`(只看配置文件里的自建目标)/ `uptimerobot`(只看 UptimeRobot 数据);也可用 `?filter=` 临时切换,顶部统计随展示集合同步
- **可配置的自动刷新**:`settings.refreshInterval`(秒)决定面板自动刷新节奏;未显式配置时,「只看 UptimeRobot」模式默认 300 秒(UR 自身 5 分钟才更新一次,刷新更快没有意义)
- **监控面板**:
- 每个服务的在线 / 离线 / 等待检测状态(绿点脉冲动画)
- 响应延迟、结果(状态码 / 端口 / 标识串)、最近检测时间(相对时间 + 悬停显示精确时间)
- 最近 48 次检测的心率图(绿色 = 在线,红色 = 离线,柱高 = 延迟)
- 分组页签筛选,顶部统计摘要(监控数 / 在线 / 离线 / 平均延迟 / 90 天可用率)
- 自动刷新倒计时(基于截止时间,不受浏览器后台标签页定时器节流影响)、手动刷新按钮、暗色模式自适应(`prefers-color-scheme`)
- **配置即文件**:`public/monitors.json` 单一配置源,改完即生效(本地探针支持热重载)
- **刷新节奏由服务端驱动**:探测由服务端按各目标 `interval` 持续进行,快照里带上「数据最后更新时刻 + 服务端更新节奏」;面板的倒计时对齐到**服务端下次更新时间**,不因访客打开页面而重新计时(旧行为下每次进页面都要重等一整个周期,最坏会看到近两倍周期的旧数据)。自动刷新只读取数据,只有手动点「刷新」才触发一次重新检测
- **UptimeRobot 双向兼容**:
- *接入*:把 UptimeRobot 已有的监控结果当数据源拉进来,与自建探测并列展示(异地视角),状态、延迟、90 天可用率、心率图全部映射自 UR 官方数据;UR 读取失败时保留上次数据,绝不把服务误判为离线
- *输出*:提供 `/api/v2/getMonitors`,字段与状态码编码对齐 UptimeRobot v2,既有脚本/看板可直接消费
- **滥用防护与信息泄露控制**:刷新接口仅内网/回环可触发、按来源冷却(429 + Retry-After)、可整体关闭;支持 `hideTargets` / `redactDetails` 与 `local.monitors.json` 私有配置分层,公开配置里可以只有服务名 —— 详见 [docs/SECURITY.md](docs/SECURITY.md)
- **主题:砂狼白子**(默认启用,可一键切回默认主题):简约风插画形象(右下角立绘 + 头像 logo/favicon,宽屏不遮挡表格)、青色光环水印、天蓝围巾飘带顶栏、倒十字发饰、公路车轮加载态。素材来源见 [主题](#主题砂狼白子)
- **三种数据源自动解析**:边缘 API(实时)→ 静态快照 → 浏览器直连,也可用 `?source=` 强制指定
## 快速开始(本地)
```bash
# 常驻模式:周期探测 + 伺服面板(默认端口 8787)
node prober/node/index.js
# 或 npm start
# 单轮探测后退出(供 CI 使用,历史心率与 90 天分桶从已有 status.json 恢复)
node prober/node/index.js --once
# 或 npm run probe
# 生成演示数据(含 90 天可用率样例,用于静态部署首版占位)
node prober/node/index.js --once --demo
# 或 npm run demo
# 自定义快照输出路径(--demo 会覆盖真实快照,需要保留真实数据时用它写到别处)
node prober/node/index.js --once --demo --out=dist/status.json
# 通知渠道自检:给每个启用的渠道发一条测试消息(配好 SMTP / MeoW 后先跑这个)
node prober/node/index.js --test-notify
# 通知状态与事件时间线的存放目录(默认 ./data,也可用环境变量 SHIROKO_DATA_DIR)
node prober/node/index.js --data-dir=/var/lib/shiroko-monitor
```
启动后打开 即可看到面板。无需安装任何依赖。
## 项目结构
```
shiroko-monitor/
├── monitors.json ─────────────── 已移至 public/monitors.json(单一配置源,见下)
├── package.json # 零依赖,仅封装常用命令
├── LICENSE # MIT
├── public/ # 纯静态面板(可整体部署到任意静态托管)
│ ├── index.html # 面板页面骨架
│ ├── monitors.json # ★ 监控目标配置(改这里)
│ ├── status.json # 探针输出的状态快照(面板读取)
│ └── assets/
│ ├── style.css # 基础样式(浅色 + 暗色自适应)
│ ├── theme-shiroko.css # 砂狼白子主题(仅在 body[data-theme=shiroko] 下生效)
│ ├── shiroko-figure.webp # 简约风立绘(AI 生成后抠底,见「主题」章节)
│ ├── shiroko-icon.webp # 简约风半身头像(同上)
│ ├── favicon-shiroko.png # 白子主题图标(由头像生成)
│ ├── shiroko-bike.webp # 简约风公路车插画(AI 生成后抠底)
│ ├── deco-scarf.svg # 自绘:天蓝围巾飘带
│ ├── app.js # 数据源解析 / 渲染 / 浏览器直连探测 / 主题切换
│ └── favicon.svg # 默认主题图标
├── prober/node/ # Node 探针(本地 / CI 用)
│ ├── index.js # 巡检循环 + 静态服务器 + /api/* + /api/v2/getMonitors + --once/--demo/--out/--config/--test-notify
│ └── lib/
│ ├── http-probe.js # HTTP 探测(fetch,redirect: manual)
│ ├── tcp-probe.js # TCP 探测(net.connect)
│ ├── banner-probe.js # Banner 探测(被动读取标识串,零认证零交互)
│ └── uptimerobot.js # UptimeRobot 只读数据源(API v2 映射)
├── scripts/ # 开发验证脚本(需 playwright)
│ ├── verify.cjs # 热重载 / 数据一致性 / 刷新语义 / 面板渲染
│ ├── verify-views.cjs # 视图渲染、切换持久化与数据源过滤
│ ├── verify-security.cjs # banner 探测 / 脱敏 / 私有分层 / 刷新限流
│ ├── verify-uptimerobot.cjs # UR 映射单测 / 兼容接口 / 失败路径 / 真实 Key 冒烟
│ ├── prepare-artwork.py # 从 assets-src/ 原图生成透明素材(边界 + 封闭区域两步抠图)
│ └── verify-layout.cjs # 布局回归:极端内容 × 13 视口 × 2 视图,断言零横向滚动
├── assets-src/ # 插画原始素材(AI 生成的白底原图,供重新处理)
├── local.monitors.json # 私有配置(不入库):真实地址、API Key
├── edge/cloudflare/ # Cloudflare Workers 边缘探针
│ ├── worker.js # cron 探针 + KV 存储 + /api/* + 静态伺服
│ └── wrangler.toml
├── .github/workflows/
│ ├── monitor.yml # 每 5 分钟跑一轮探针并提交 status.json
│ └── pages.yml # 推送 main 时部署 GitHub Pages
└── docs/
├── DEPLOYMENT.md # 四种部署方案详细步骤
├── SECURITY.md # 刷新滥用防护 / SSH 探测边界 / 信息泄露控制
└── screenshot-*.png # 各视图实测截图
```
> 配置源为什么放在 `public/` 下:面板用相对路径 `fetch('monitors.json')`,探针(Node/Worker)、静态托管(Pages)读取的都是同一份文件,零拷贝、零漂移。配置文件不含任何密钥,可安全公开。
## 配置说明(public/monitors.json)
```jsonc
{
"settings": {
"title": "Shiroko Monitor", // 面板标题
"description": "轻量级服务监控面板",
"view": "table", // 默认视图:table | cards
"theme": "shiroko", // shiroko(砂狼白子)| default
"artwork": "", // 立绘图片路径(相对 public/),留空则不显示
"decoOpacity": 0.78, // 人物/单车装饰的全局透明度(0~1,一个值同时作用于两者)
"dataFilter": "both", // 展示口径:both | config | uptimerobot(详见「面板形态与数据源」)
"probeScope": "all", // 探测范围:all | config | uptimerobot;不填则**跟随 dataFilter**
"refreshInterval": 30, // 面板检查数据的最大间隔(秒,最小 5);实际对齐服务端更新节奏
"dataSource": "auto", // auto | api | static | browser
"allowRefresh": true, // 是否允许触发强制探测(公网部署建议 false;详见 docs/SECURITY.md)
"refreshLocalOnly": true, // 仅内网/回环来源可触发强制探测
"refreshCooldownSec": 5, // 每个来源的触发冷却(秒)
"hideTargets": false, // true = 面板不渲染任何地址(对外发布用)
"redactDetails": false, // true = 快照 detail 只保留「正常/异常」
"notify": { // 通知:故障/恢复/证书到期(SMTP 密码与 MeoW 昵称请放 local.monitors.json)
"enabled": false,
"failureThreshold": 2,
"quietHours": "",
"channels": { "email": { "enabled": false }, "meow": { "enabled": false } }
},
"tls": { "enabled": true, "checkIntervalSec": 21600, "alertDays": [14, 7, 3] },
"maintenance": [], // 维护窗口:[{ from, to, id?, note? }]
"uptimeRobot": { // UptimeRobot 只读数据源(Key 建议放 local.monitors.json)
"enabled": false,
"apiKey": "",
"pollIntervalSec": 300,
"group": "UptimeRobot"
}
},
"monitors": [
{
"id": "example-com", // 唯一 ID,状态数据按此关联
"name": "Example 站点", // 显示名称
"group": "公共服务", // 分组名(相同名字归为一组)
"type": "http", // http | tcp | banner
"url": "https://www.example.com",
"method": "GET", // 可选,默认 GET
"expectStatus": [200], // 判定为「在线」的状态码列表
"interval": 60, // 探测间隔(秒);无人打开面板时的后台节奏
"timeout": 10, // 超时(秒)
"probeOnRefresh": false // 可选:false = 该目标不参与「刷新即重新检测」,只按自己的 interval 探测
},
{
"id": "cloudflare-dns-tcp",
"name": "Cloudflare 443 端口",
"group": "基础设施",
"type": "tcp", // 只测端口连通性
"host": "1.1.1.1",
"port": 443,
"interval": 60,
"timeout": 10
},
{
"id": "ssh-gateway",
"name": "跳板机 SSH",
"group": "基础设施",
"type": "banner", // 被动读取标识串(SSH/SMTP/FTP/POP3/IMAP/HTTP…)
"host": "10.0.0.5",
"port": 22,
"expectBanner": "^SSH-2\\.0-", // 可选:正则策略,不匹配即判离线(可做版本合规检查)
"bannerFlags": "i", // 可选:正则 flags,默认 i
"revealBanner": false, // 默认 false:只输出「SSH 响应正常」,不回显版本号
"interval": 300,
"timeout": 5
}
]
}
```
添加监控目标 = 在 `monitors` 数组里加一项,保存即可(本地探针自动热重载;静态部署等下一次探针写入)。
`interval` 与 `refreshInterval` 的关系:
- `interval`(每个目标):**服务端**的探测节奏,与是否有访客无关;也决定 90 天可用率的样本量
- `refreshInterval`(面板):面板去服务端取数据的最长间隔。由于倒计时对齐服务端更新节奏,页面打开期间**不会**额外触发探测;服务端数据比我配置的节奏更新更快时,面板会跟着服务端走
- 探测只由服务端驱动 → 打开再多标签页也不会增加对目标的压力(旧版本下每个访客都会触发探测轮次)
- 需要立刻看一次实时结果时,手动点面板上的「刷新」(服务端限流/关闭策略见 [docs/SECURITY.md](docs/SECURITY.md))
> ⚠️ **注意目标限流**:探测本身会消耗目标的配额。例如 GitHub 未认证 API 每小时仅 60 次,按 30s 节奏探测会很快耗尽并返回 403,面板会如实记为离线(那不是面板故障,而是服务真的在拒绝请求)。对这类目标请调大 `interval`,并可设目标级 `probeOnRefresh: false` 让它退出强制检测。仓库默认配置里的 `github-api` 就是这样一个示例目标。
## 数据源模式
| 模式 | 触发条件 | 数据新鲜度 | 说明 |
| --- | --- | --- | --- |
| `api` | 同源存在 `/api/status`(Node 服务器 / Cloudflare Worker) | 实时 | 首选 |
| `static` | 存在 `status.json`(GitHub Pages / EdgeOne 静态部署) | 取决于探针写入频率 | 次选 |
| `browser` | 以上都不可用 | 实时(浏览器直连) | 兜底,限制见下 |
`auto` 模式按上表顺序自动降级。可用 `?source=api|static|browser` 强制指定,便于调试。
## UptimeRobot 集成
### 一、把 UptimeRobot 当数据源(推荐)
UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自建探测并列。配置(**API Key 请放 `local.monitors.json`,不要写进公开配置**):
```jsonc
// local.monitors.json(已在 .gitignore 中)
{
"settings": {
"uptimeRobot": { "enabled": true, "apiKey": "ur123456-xxxxxxxxxxxxxxxxxxxxxxxx" }
}
}
```
```jsonc
// public/monitors.json 的 settings 里保留开关与默认值
"uptimeRobot": {
"enabled": false, // 公开配置默认关闭;由私有配置开启
"apiKey": "", // 留空;也可用环境变量 UPTIMEROBOT_API_KEY(适合 CI/Secrets)
"pollIntervalSec": 300, // 拉取间隔,别低于 UR 的检测间隔(免费版 5 分钟)
"group": "UptimeRobot", // 镜像目标在面板中的分组名
"includeTypes": [] // 可选,按 UR 类型过滤:1=HTTP 2=Keyword 3=Ping 4=Port
}
```
映射关系(全部来自 UR 官方 API,非我们的二次探测):
| 面板字段 | UptimeRobot 来源 |
| --- | --- |
| 在线/离线/等待 | `status`:2→在线,8/9→离线,0/1→等待检测 |
| 响应延迟 | 最新一条 `response_times`(缺失时用 `average_response_time`) |
| 心率图 | `response_times`(最多 48 个采样点,随 `response_times_average=30` 聚合) |
| 90 天可用率 | `custom_uptime_ratio`(1/7/30/90 天),逐日色条由本探针同步后自行累积 |
| 结果/原因 | 故障时取最近一次故障日志的 `reason.code/detail`,如 `403 Forbidden` |
| 类型徽标 | `UR·HTTP` / `UR·PORT` / `UR·KEYWORD`(与自建探测区分) |
行为约定:
- **只读**:仅调用 `getMonitors`,不做任何写操作,因此 `ur*` 开头的只读 Key 即可
- **不参与本地强制探测**:UR 镜像由 UR 自己探测,本探针只拉结果;面板「刷新」不会去跑它们
- **失败不误判**:Key 无效、限流、网络异常时只记录错误(页脚显示「UptimeRobot 同步异常」并给出原因),目标状态保持上一次的值
- 速率:一次请求取回全部监控,默认 5 分钟一次(12 次/小时),远低于 UR 的接口限制
### 二、对外提供 UptimeRobot 兼容接口
`GET|POST /api/v2/getMonitors` 返回与 UptimeRobot v2 同构的响应,便于已有脚本/看板迁移:
```jsonc
{
"stat": "ok",
"pagination": { "offset": 0, "limit": 50, "total": 14 },
"monitors": [{
"id": 536986782, // 由字符串 id 派生的稳定数字 id
"friendly_name": "Example 站点",
"url": "https://www.example.com",
"type": 1, // 1=HTTP/Keyword 4=Port(UR 编码)
"status": 2, // 2=在线 9=离线 1=未检测
"interval": 60,
"custom_uptime_ratio": "97.436-97.436-97.436-97.436", // 1-7-30-90 天,三位小数
"average_response_time": 1291,
"response_times": [{ "datetime": 1791880740, "value": 429 }]
}]
}
```
该端点不做鉴权(其内容与公开面板一致);如需鉴权请在其前方加代理。`api_key` 参数会被忽略。
## 面板形态与数据源
### 视图
| 视图 | 形态 | 适合 |
| --- | --- | --- |
| `table` 表格 | 一行一服务,列含状态/名称/目标/延迟/结果/最近检测/心率/可用率 | **默认**,目标多时可一屏扫完 |
| `cards` 卡片 | 一服务一卡 + 心率图 + 90 格可用率条 | 需要更大点击区域或看图细节 |
指定方式(优先级从高到低):URL 参数 `?view=table|cards` → 浏览器上次选择(localStorage)→ `settings.view` → 表格。
**移动端与窄窗口**:视口窄于 **1024px**(表格含心率图与可用率条,实测最小舒适宽度约 1010px)且用户没有显式选择过视图时,**自动使用卡片**;横竖屏切换时重算,显式选择(`?view=` 或点过切换器)始终优先。顶栏在 ≤820px 改为「标题 / 统计 / 控件」三行,立绘与固定装饰自动撤掉。
### 数据源过滤
| 值 | 含义 |
| --- | --- |
| `both` | 配置文件里的自建目标 + UptimeRobot 镜像目标都展示(默认) |
| `config` | 只看配置文件里的自建探测目标 |
| `uptimerobot` | 只看 UptimeRobot 的数据 |
配置在 `settings.dataFilter`;也可用 URL 临时覆盖(适合做固定链接,如挂墙看板):`?filter=uptimerobot`。
过滤只影响**展示与快照输出**,不影响探测——配置文件里的目标始终按各自 `interval` 探测。
顶部统计(监控数/在线/离线/平均延迟/可用率)按当前展示的集合实时汇总,不会出现「列表 8 行、统计 14 个」的不一致。
### 自动刷新节奏(服务端驱动)
```jsonc
"refreshInterval": 300 // 秒,最小 5;不配则默认 30,「只看 UptimeRobot」时默认 300
```
面板不再从「你打开页面的那一刻」重新计时,而是**对齐服务端的更新节奏**:
| 环节 | 由谁驱动 | 说明 |
| --- | --- | --- |
| 数据产出 | **服务端** | 自建目标按各自 `interval` 探测;UptimeRobot 按 `pollIntervalSec` 拉取。与是否有访客无关 |
| 面板检查时刻 | 对齐服务端 | 快照带 `dataUpdatedAt`(数据最后变更时刻)与 `updateIntervalSec`(服务端更新节奏),面板在「上次更新 + 一个周期」时去取一次 |
| `refreshInterval` | 上限 | 面板检查间隔不会超过它;服务端节奏更快时按服务端节奏走 |
| 手动「刷新」 | 你 | 唯一会触发**立即重新检测**的动作(服务端可拒绝/限流,见 docs/SECURITY.md)|
| 自愈 | 面板 | 若服务端数据超过 1.5 个周期没更新(如探针刚重启或被休眠),面板会自动触发一次重新检测 |
效果:无论你什么时候打开页面,倒计时都显示「距服务端下一次更新还有多久」——刚更新完打开是 300s,过了 4 分钟打开就只剩 60s。静态快照与浏览器直连模式没有可对齐的服务端节奏,仍按 `refreshInterval` 轮询。
## 主题(砂狼白子)
面板名称叫 shiroko-monitor,主题自然取了《蔚蓝档案》的**砂狼白子**:她的光环、围巾、狼耳与配色都被做进了界面。人物形象用的是**简约风插画**(扁平配色、无阴影,见下),不是官方立绘。
### 主题里的元素
| 位置 | 元素 | 取自设定 |
| --- | --- | --- |
| 左上 logo / 浏览器图标 | **简约风半身头像**(光环 + 狼耳 + 围巾)+ 青色光环环(呼吸式外扩动画) | 生成插画的半身裁切 |
| 右下角 | **简约风全身立绘**(透明底,窄身设计:实测 1280 / 1440 / 1920 下都不遮挡表格,页面也无横向滚动);全局半透明(`settings.decoOpacity`,默认 0.78)+ 上下渐隐,融入背景 | 生成插画 |
| 顶栏底边 | **天蓝色围巾飘带**:波浪形状 + 5 处褶皱纹路(自绘) | 天蓝色围巾是星野送给她的信物 |
| 页脚下方 | **光环水印**(自绘:倾斜椭圆环 + 四方位带圆头的短杆) | 白子的光环是倾斜的椭圆环,四个方位各有一条带圆头的短杆,整体形似全息瞄准镜准星 |
| 状态点 | **迷你光环**(外圈 + 中心点,在线时缓慢脉冲外扩) | 光环是基沃托斯学生的标志 |
| 分组标题 | **青色倒十字发饰**(旋转 45°,自绘) | 她左侧刘海上别的青色十字发夹 |
| 刷新按钮加载态 | **公路车轮**(轮圈 + 辐条 + 花鼓)旋转 | 她最爱的公路车(Bianchi ARIA 原型) |
| 左下角 | 公路车插画(简约扁平风,与人物同一套视觉语言;与人物共用同一个全局透明度 `decoOpacity` + 两端渐隐) | 她最爱的公路车(Bianchi ARIA 原型) |
| 配色 | 天蓝 / 青 + 藏蓝 + 灰白;「正常」色改为青绿(`#12b0a2`) | 阿拜多斯藏蓝校服 + 天蓝围巾 + 灰白发色 |
左下公路车、页脚光环、右下立绘构成页脚下方的一条装饰带,三者互不遮挡。
### 素材来源
| 类别 | 内容 | 来源 |
| --- | --- | --- |
| **插画(人物形象 + 座驾)** | `shiroko-figure.webp`(249×560)、`shiroko-icon.webp`(180×180)、`favicon-shiroko.png`(64×64)、`shiroko-bike.webp`(341×200) | **AI 生成**:按白子的设定要素(灰发狼耳 / 倾斜椭圆光环 / 天蓝围巾 / 藏蓝校服 / 公路车)描述生成,再用 Pillow 抠掉白底转为透明。**不是**官方美术素材,可自由替换或商用自行判断 |
| 自绘装饰(SVG/CSS) | 围巾飘带 `deco-scarf.svg`、光环水印、迷你光环状态点、倒十字图标、车轮加载图标 | 本仓库原创,按公开设定还原 |
| 设定资料(光环形状、发饰、围巾、配色、车型) | 设计依据 | [萌娘百科 · 砂狼白子](https://mzh.moegirl.org.cn/%E7%A0%82%E7%8B%BC%E7%99%BD%E5%AD%90)、[Blue Archive Wiki](https://bluearchive.fandom.com/zh/wiki/%E7%A0%82%E7%8B%BC%E7%99%BD%E5%AD%90)、[百度百科](https://baike.baidu.com/item/%E7%A0%82%E7%8B%BC%E7%99%BD%E5%AD%90/62839164) |
| 曾用素材(已移除) | 最早一版用过官方立绘与头像,为改成简约形象已从仓库删除 | [SchaleDB](https://schaledb.com):`images/student/portrait/10010.webp`、`images/student/icon/10010.webp`(Id `10010` = 砂狼白子),版权归 NEXON Games / Yostar;想换回官方图可再从该地址取 |
蔚蓝档案 / Blue Archive © NEXON Games · Yostar —— 角色与设定版权归原作者所有,本主题是个人自用面板的风格致敬。
### 切换与配置
```jsonc
"theme": "shiroko", // shiroko | default(默认主题);也可用地址栏 ?theme=default 临时覆盖
"artwork": "assets/shiroko-figure.webp" // 立绘图片路径(相对 public/),留空则不显示
```
优先级:`?theme=` → 浏览器上次选择(localStorage)→ `settings.theme`。顶栏右侧的光环图标按钮可一键切换。切回默认主题后,立绘、光环、斜纹、围巾飘带、公路车全部消失,回到最初的简洁样式。
**换成你自己的图**:把图片放进 `public/assets/`,再把 `artwork` 指向它即可(透明底 PNG/WebP 效果最好)。文件不存在时静默不显示,不会出现破图。
## 90 天可用率
探针每完成一次探测,就把它计入「当日分桶」:`monitors..daily["YYYY-MM-DD"] = { total, up }`,只保留最近 90 天(超出自动淘汰)。面板据此渲染 90 格色条与百分比。
- **数据积累**:从探针首次运行当天开始累积,跑满 90 天后色条填满;没有数据的日期显示灰色(悬停提示「无数据」)
- **判定口径**:可用率 = 当日在线探测次数 / 当日总探测次数。**提高探测频率会提高统计精度**(60s 间隔约 1440 次/天,GitHub Actions 5 分钟粒度约 288 次/天)
- **本地时区**:Node 探针按运行机器的本地日期分桶;Cloudflare Worker 无本地时区概念,按 UTC 日期分桶
- **跨重启保留**:分桶随 `status.json` 持久化,重启/CI 单轮模式都会先恢复再追加,不会断档
- **浏览器直连模式**:不记录历史,色条显示灰色、「可用率数据积累中」——需要可用率请使用 Node 探针或边缘 Worker
- **`--demo`** 会生成 90 天演示分桶(含绿/橙/红各档),仅用于界面预览;若不想覆盖真实快照,用 `--out=` 写到别处
## 通知、证书与事件
### 通知(邮件 + MeoW)
面板只在你打开时展示状态,**通知**让故障主动找你。支持两个渠道:**邮件**(自写 SMTP 客户端,零依赖)与 **MeoW 推送**(按 [官方文档](https://www.chuckfang.com/MeoW/api_doc.html) 实现)。
```jsonc
"notify": {
"enabled": true,
"failureThreshold": 2, // 连续失败 N 次才告警(去抖,默认 2;1 = 立即)
"recoveryNotify": true, // 恢复时也发一条
"quietHours": "23:00-07:00", // 静默时段(本地时间,支持跨零点),期间只记录不发送;留空全天
"channels": {
"email": {
"enabled": true,
"host": "smtp.example.com", "port": 465, // 465 隐式 TLS;587/25 自动 STARTTLS
"user": "monitor@example.com", "pass": "密码",
"from": "monitor@example.com", "to": ["ops@example.com"],
"allowInsecure": false // 服务器不支持 STARTTLS 时是否允许明文(默认拒绝)
},
"meow": {
"enabled": true,
"nickname": "你的昵称", // MeoW 以昵称寻址、无需 token,因此昵称等同密钥
"baseUrl": "https://api.chuckfang.com",
"msgType": "text" // text | html(html 可另配 htmlHeight)
}
}
}
```
行为要点:
- **去抖**:连续失败达到阈值才发「故障」;恢复时发「恢复」;同一轮多个目标变化会合并成一条,不刷屏
- **重启不重复**:去抖状态持久化在 `data/notify-state.json`(私有目录,已 gitignore),重启/单轮模式都不重发
- **静默时段**:支持 `23:00-07:00` 这类跨零点写法;静默期内照常记录事件,只是不发送
- **MeoW 限流**:客户端本地也做限流(3 秒 1 条 / 15 每分钟 / 60 每小时),替你挡掉一部分官方 429;429/404/403 都会转成可读的错误原因
- **CI 注意**:MeoW 官方明确不建议用多人共享的出口 IP(GitHub Actions 即属此类),CI 场景建议只开邮件渠道
- **自检**:`node prober/node/index.js --test-notify` 给每个启用渠道发一条测试消息,确认配置可用
> SMTP 密码与 MeoW 昵称属于私密信息,请放在 `local.monitors.json`(已 gitignore),不要写进公开的 `monitors.json`。
### TLS 证书监控
对 `https://` 目标,探针额外做一次 TLS 握手读取证书有效期(默认 6 小时一次):
- 面板在名称旁显示徽标:剩余 **≤14 天**黄色、**≤7 天**红色,悬停可见到期日与签发者
- 剩余 ≤14 / 7 / 3 天时按阈值告警;同一张证书每个阈值只发一次,换证书后重新计算;**已过期的证书只报最严重的一档**
- 单个目标不需要可设 `"tls": false`
```jsonc
"tls": { "enabled": true, "checkIntervalSec": 21600, "alertDays": [14, 7, 3] }
```
### 维护窗口
计划重启/迁移期间,不想污染可用率统计、也不想被告警轰炸:
```jsonc
"maintenance": [
{ "from": "2026-10-14T02:00", "to": "2026-10-14T04:00", "id": "box", "note": "升级迁移" }
]
```
- `id` 可省略(对所有目标生效),也支持写成数组
- 窗口内:探测照常进行,但**不计入当日可用率**、**不发告警**,面板显示「维护中」徽标;事件时间线照常记录并带 `maintenance: true`
### 事件时间线
每次状态翻转(故障 / 恢复 / 证书阈值)都会追加到 `data/events.json`(私有目录,保留最近 500 条),并通过 `GET /api/events` 读取。快照里也会带上最近 20 条供面板使用;`redactDetails` 开启时,快照内事件只保留类型与时间。
**面板展示**:监控列表下方有一个「最近事件」区块(最多 20 条),每行显示 `时间 / 类型 / 目标 / 详情`;维护窗口内的事件会带「维护」标记(表示未计入可用率、未发告警)。无事件时区块自动隐藏。
### 响应时间趋势
当日分桶在 `{ total, up }` 之外还累计 `latSum / latCount / latMax`(仅在线样本),探针据此在快照里给出 `lat: { avg, max, days: [{ key, avg }] }`:
- **卡片视图**:可用率条下方多一条「90 天日均延迟」细柱图(取最近 30 天),悬停看单日数值,右侧为 90 天均值
- **表格视图**:可用率列的 tooltip 里追加 `90 天日均延迟 X ms(峰值 Y ms)`
- 数据从探针开始累计当天起累积,跑满 90 天后覆盖全周期;离线样本不计入延迟统计
## 管理后台(2FA 保护)
面板右侧没有「编辑」入口:所有会改配置的操作都放在独立的管理后台 `/admin`,**必须绑定并通过两步验证才能进入**。
### 启用
```bash
# 1. 设置绑定口令(只在首次绑定时需要,用于防止被人抢先绑定自己的验证器)
export SHIROKO_ADMIN_SETUP_KEY='换成你自己的长口令'
node prober/node/index.js
# 2. 打开 http://:8787/admin
```
不设置 `SHIROKO_ADMIN_SETUP_KEY` 时绑定入口直接关闭(接口返回 503),管理后台处于「完全不可用」状态。
### 绑定流程
1. 输入 `SHIROKO_ADMIN_SETUP_KEY` → 服务端生成一个 20 字节 Base32 密钥
2. 用 Google Authenticator / Microsoft Authenticator / 1Password / Aegis 扫码,或手动输入密钥(页面同时提供可复制的 `otpauth://` 链接)
3. 输入 App 显示的 6 位验证码完成绑定
4. **立刻保存 10 枚恢复码**(只显示这一次),每枚只能用一次,用于手机丢失或验证器清库
绑定完成后每次进入管理界面都需要重新输入 6 位动态码或一枚恢复码。
### 管理功能
| 功能 | 说明 |
| --- | --- |
| 运行状态 | 监控数 / 在线 / 离线 / 平均延迟 / 90 天可用率,数据更新时间,各数据源目标数与同步状态 |
| 探测范围 | `all` / `config` / `uptimerobot`,决定探针真正发请求的范围,改动立即热重载 |
| 展示口径 | `all` / `config` / `uptimerobot`,只影响面板展示,与探测范围相互独立 |
| 维护窗口 | 新增/删除窗口,可限定单个目标或全部目标;窗口内不计入可用率、不发告警 |
| 立即重新探测 | 手动跑一轮探测(复用面板刷新同一条路径) |
| 通知自检 | 给每个已启用渠道发一条测试消息 |
| 监控目标 | 只读列表(名称 / 分组 / 类型 / 地址 / 间隔) |
**所有写操作只落 `local.monitors.json`(私有、不入库),绝不改 `public/monitors.json`。**
### 安全设计
- **未绑定时管理接口一律 403**,不存在「先跳过 2FA 进管理界面」的路径
- 会话 id 为 32 字节随机数,**服务端只存 SHA-256 哈希**;进程重启即清空全部会话
- 会话 cookie 为 `HttpOnly; SameSite=Strict`,30 分钟无操作失效,最长 2 小时
- 同一时间步的 6 位码**用过即作废**(防截图重放);恢复码一次性,用后立刻从落盘档案中移除
- 登录失败按 IP 计数,连续 5 次失败后指数退避锁定(1m → 2m → 4m …上限 30m)
- 只信任 socket 对端地址,**不解析 `X-Forwarded-For`**(可被伪造)
- 2FA 档案存 `data/admin-2fa.json`(`data/` 已在 `.gitignore` 中),恢复码只存哈希
- 丢失全部验证码时的逃生口:用 `SHIROKO_ADMIN_SETUP_KEY` 重置 2FA(会清空密钥与全部恢复码,需重新绑定)
> 部署到公网时建议放在 HTTPS 反向代理之后:会话 cookie 未设 `Secure`(纯 HTTP 下设了会导致 cookie 完全不发送),由代理层负责加密传输。
### 面板上的管理入口(`settings.adminUrl`)
如果你把管理端放在别的地址(内网 IP、Tailscale、VPS 域名、隧道……随便哪种),可以在管理界面里把这个地址填进**「面板上的管理入口链接」**,公开面板的页脚就会出现一个「管理」按钮指向它。
- **留空 = 面板不显示任何管理入口**(默认状态,避免在公网面板上暴露管理面的位置)
- 地址只接受 `http` / `https`,其它协议(`javascript:` 等)会被拒绝
- 写入位置是 `local.monitors.json`(私有层),所以填了之后**只有你自己的面板**会出现这个按钮
面板本身不关心你怎么暴露管理端 —— 它只负责「你填什么就跳到什么」。
### 让管理端能被外部访问
探针**默认只监听 `127.0.0.1`**,外部访问不到。需要时用 `settings.admin.allowRemote` 打开:
```jsonc
// local.monitors.json
{ "settings": { "admin": { "allowRemote": true } } }
```
打开后探针监听 `0.0.0.0`,启动日志会就此告警。**怎么把流量送到这个端口由你决定** —— 反向代理、隧道、VPN、直连公网 IP 都行,本项目不做任何假设。
> ⚠️ 注意:打开后同一端口上的公开面板(`/` 与 `/api/status`)也会跟着对外可达。它们本身是只读的,但目标名称、地址与可用率会一并暴露。如果不希望这样,请在你自己那层(反向代理/隧道)按路径收窄。
### 面板部署在别处(跨域只读)
> 完整步骤见 **[docs/DEPLOY-PAGES.md](docs/DEPLOY-PAGES.md)**(GitHub Pages 面板 + 本地探针的部署指南,含常见问题排查)
面板与探针可以放在两台机器上:面板放 GitHub Pages / Cloudflare(静态托管),探针放你家里或内网的机器上持续探测。两项配置:
| 配置 | 作用 |
| --- | --- |
| `settings.dataUrl` | 面板去哪里取数据,例 `https://monitor.example.com`。留空表示与探针同源 |
| `settings.corsOrigins` | 允许跨域读取的**页面地址**,逗号分隔的完整 origin,如 `https://you.github.io` |
两者都在管理界面里填(改完立即生效)。
**硬限制(务必先看)**:GitHub Pages 强制 HTTPS,而浏览器的**混合内容**策略禁止 HTTPS 页面 `fetch` HTTP 资源 —— 这与 CORS 无关,改设置也没用。**因此 `dataUrl` 必须与面板页面同为 HTTPS**(你自己搭隧道给本地探针套 HTTPS 即可)。
安全设计:
- **绝不使用 `Access-Control-Allow-Origin: *`** —— 那等于让任意网站都能读走你的监控数据。默认只放行本机回环地址(`localhost` / `127.0.0.1`,**忽略端口**,因为本地前端常跑在 5173 这类端口)
- 白名单里的 origin 必须**精确匹配**,`https://you.github.io.evil.com` 这种前缀伪造会被拒
- 回显具体 origin 并带 `Vary: Origin`,避免中间缓存串味
- **管理端永不放行跨域**:`/admin` 与 `/api/admin/*` 不带任何 `Access-Control-*` 头,外部页面无法读取也无法写入
- 跨域读到的配置同样经过密钥剥离
### 部署形态与可写能力
| 部署形态 | 面板可读 | 探测 | 通知/维护窗口/事件 | 管理端(2FA) |
| --- | --- | --- | --- | --- |
| 本地 Node | ✅ | ✅ | ✅ | ✅ |
| Cloudflare Workers | ✅ | ✅(含 UptimeRobot 数据源) | ❌ | ❌ 403 |
| GitHub Pages / EdgeOne Pages | ✅ | ❌(仅展示 CI 快照) | ❌ | ❌ 不存在 |
**管理端只存在于「本地 Node」形态。** 边缘环境访问 `/admin` 或 `/api/admin/*` 会得到 403 + 指向文档的提示(而非 404,明确表达「这里不提供写能力」)。
原因是这些能力依赖本地状态:2FA 档案与会话存在 `data/` 目录与进程内存里,而 Workers 无文件系统、isolate 之间不共享内存(KV 又是最终一致,无法承载「恢复码一次性使用」这类语义)。
## 部署方案
| 方案 | 架构 | 数据更新 | 成本 | 适合场景 |
| --- | --- | --- | --- | --- |
| 本地 Node | 探针 + 面板同机伺服 | 实时(按 interval) | 免费 | 内网自用、开发调试 |
| GitHub Pages + Actions | 纯静态 + Actions 定时提交 status.json | 每 5 分钟(Actions 下限) | 免费 | 公网演示、个人项目 |
| Cloudflare Workers | 边缘函数 cron 探针 + KV | 每 1 分钟 | 免费版可用(TCP 需付费版) | 公网生产、多目标 |
| EdgeOne Pages | 纯静态 + 数据更新同 Pages 路径 | 每 5 分钟 | 免费额度 | 国内访问加速 |
四种方案的详细步骤见 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)。
## 浏览器直连模式的限制(重要)
浏览器没有原始 TCP 能力,且受同源策略约束,`browser` 模式下:
- HTTP:优先以 `cors` 模式请求,目标响应带 `Access-Control-Allow-Origin` 时可读到真实状态码;否则降级 `no-cors`,**只能判断可达性,读不到状态码**(显示「跨域可达」)
- TCP:以 HTTP 连接行为近似——快速拒绝判为端口关闭;连接后无响应直到超时判为端口开放(启发式,无法区分「开放但静默」与「被防火墙丢弃」)
- HTTPS 页面无法探测 HTTP 目标(浏览器混合内容拦截,会判为离线)
- 结论:**浏览器直连只作兜底**。要准确的状态码与 TCP 探测,请使用 Node 探针或边缘 Worker 提供 `/api/status`
## 已实现 vs 路线图
### v0.1.0 已实现
- [x] HTTP 探测:状态码 / 延迟 / 自定义期望值 / 超时 / 保留 3xx
- [x] TCP 探测:连通性 / 连接耗时 / 拒绝与超时区分
- [x] Node 探针:常驻巡检、周期调度、配置热重载、历史心率持久化(重启不断档)、原子写入 status.json
- [x] Node 探针附带静态服务器与 `/api/status`、`/api/monitors`、`/api/refresh`
- [x] `--once`(CI 单轮)与 `--demo`(演示数据)模式
- [x] 面板:状态卡片、心率图、分组页签、统计摘要、相对/绝对时间、自动刷新倒计时、暗色模式
- [x] 数据源三级自动降级 + `?source=` 强制指定
- [x] Cloudflare Worker 边缘探针(cron + KV + 静态伺服)
- [x] GitHub Actions:定时探测 + Pages 部署
- [x] 四种部署路径文档
### v0.2.0 已实现
- [x] **90 天可用率**:按天分桶聚合、90 天滚动保留、卡片 90 格分级色条 + 悬停明细、顶部整体可用率(Node 与 Worker 双端一致实现)
- [x] `--out=` 自定义快照输出路径;`--demo` 覆盖真实快照前给出告警
- [x] 修复:配置热重载后 `/api/status` 永久冻结在旧数据(闭包捕获了被替换的旧 state 引用,改为持有器对象)
- [x] 修复:慢目标(如 10s 超时)会被每秒的调度 tick 重复探测,形成探测风暴(改为探测前同步占位 + inFlight 标记)
- [x] 修复:非法/缺失 `interval` 会算出 `NaN` 导致该目标永久不再被调度(改为回落默认 60s)
- [x] 修复:刷新倒计时改按截止时间计算,不再受浏览器后台标签页定时器节流影响
- [x] `scripts/verify.cjs` 端到端回归验证(15 项断言,覆盖上述修复)
### v0.2.1 已实现
- [x] **刷新即重新检测**:`/api/refresh` 等待本轮探测完成后返回最新快照,面板自动刷新与手动「刷新」按钮都会真正重新探测目标(此前只是重读旧快照,导致「部分卡片停留在几十秒前、手动刷新也没变化」)
- [x] 服务端强制轮次去重:多标签页/多客户端同时刷新共享同一轮探测,2s 内的重复请求直接复用快照(避免对目标重复施压);强制轮次先等在途探测收尾,保证不跳过任何目标
- [x] `settings.probeOnRefresh`(默认 `true`):关闭后自动刷新退回只读轮询,手动刷新仍然强制检测
- [x] 目标级 `probeOnRefresh: false`:单个目标可退出「刷新即检测」(适合有限流的 API),只按自身 `interval` 探测;默认配置新增示例目标说明该用法
- [x] Cloudflare Worker 的 `/api/refresh` 同步改为「等待完成再返回」+ 同隔离体去重
### v0.3.0 已实现
- [x] **Banner 探测**(`type: banner`):被动读取 SSH/SMTP/FTP/POP3/IMAP 等标识串,`expectBanner` 正则策略校验(可做版本合规)、`revealBanner` 显式开启才回显;不做认证、不发数据,不触发目标封禁
- [x] **四种面板形态**:卡片 / 表格 / 大屏(深色 NOC)/ 极简(对外状态页),顶部一键切换 + `?view=` + `settings.view` + localStorage 记忆
- [x] **刷新接口滥用防护**:内网/回环来源限制(不信任 X-Forwarded-For)、每来源冷却 429 + Retry-After、轮次去重、总开关 `allowRefresh`;Worker 需 `REFRESH_TOKEN` 才开放
- [x] **信息泄露控制**:`local.monitors.json` 私有配置分层(公开配置可只留 id/name/group)、`hideTargets`、`redactDetails`、TCP 详情不再回显地址、banner 默认不回显版本
- [x] 私有配置分层 + `--config` / `--local-config`(多实例与测试用)
- [x] 修复:客户端中断请求(切页/关标签)后向已断开 socket 写响应会抛出未捕获异常,可能终止探针进程
- [x] 修复:非卡片视图的状态点沿用胶囊色(着色规则作用域写死在 `.card` 上)
- [x] 修复:首次打开面板时需等强制检测完成才出数据,期间显示空白「等待检测」——改为先用已有快照立即渲染
- [x] 新增 `docs/SECURITY.md`(滥用防护 / SSH 探测边界 / 信息泄露控制)与 `scripts/verify-security.cjs`、`scripts/verify-views.cjs`
### v0.4.0 已实现
- [x] **UptimeRobot 数据源(只读)**:把 UR 已有监控作为「异地视角」接入面板,状态/延迟/心率/90 天可用率/故障原因全部映射自 UR 官方字段;镜像目标不参与本地强制探测;同步失败只记录错误并保留上次数据
- [x] **UptimeRobot 兼容接口**:`/api/v2/getMonitors` 输出与 UR v2 同构(含 `custom_uptime_ratio`、`response_times`、数字 id 与状态码编码)
- [x] `stats.memory`:探针自报内存占用(RSS/heapUsed),方便观察「监控器自身」的开销
- [x] 配置分层增强:`--config` / `--local-config` 指定配置文件路径;UR API Key 支持私有配置或 `UPTIMEROBOT_API_KEY` 环境变量
- [x] 修复:非 http/tcp 类型的目标行渲染为 `undefined:undefined`(改为有 url 用 url、有 host 用 host:port)
- [x] 修复:热重载会丢弃 UptimeRobot 镜像目标已累积的逐日分桶
- [x] 修复(实测发现):`custom_down_durations` 不能作为请求参数传给 UR v2(返回 `invalid_parameter`),改为只从响应中读取
- [x] 新增 `scripts/verify-uptimerobot.cjs`(22 项:映射单测 / 兼容接口 / 失败路径 / 真实 Key 冒烟)
### v0.5.0 已实现
- [x] **表格设为默认视图**,移除大屏与极简两种形态(面板只保留表格/卡片,切换器更精简)
- [x] **数据源过滤** `settings.dataFilter`:`both` / `config`(仅配置文件)/ `uptimerobot`(仅 UR),支持 `?filter=` 临时覆盖;服务端快照与面板共用同一口径(私有配置覆盖后也不会错位)
- [x] **顶部统计按展示集合汇总**:切换过滤后不再出现「列表 8 行、统计 14 个」
- [x] **自动刷新间隔可配置**并按数据源自适应:`refreshInterval`(秒);只看 UptimeRobot 时默认 300 秒(UR 自身 5 分钟才更新)
- [x] `/api/refresh?scope=external`:只看 UR 的刷新只同步 UR 数据,不跑本地探测轮次;普通刷新也会顺带同步 UR(最小间隔 60 秒,尊重 UR 接口限流)
### v0.6.0 已实现
- [x] **刷新节奏改为服务端驱动**:快照新增 `dataUpdatedAt`(数据最后变更时刻)与 `updateIntervalSec`(服务端更新节奏),面板倒计时对齐到服务端下次更新,**不再每次打开页面从整周期重新计时**(旧行为最坏会看到近两倍周期的旧数据;实测:两次打开间隔 25s,倒计时差值 28s)
- [x] 自动刷新改为**只读**:探测完全由服务端按 `interval` / `pollIntervalSec` 驱动,访客数量不再影响对目标的压力;只有手动点「刷新」才触发立即重新检测
- [x] 新增自愈:服务端数据超过 1.5 个周期未更新时(探针刚重启、机器休眠过)面板自动触发一次重新检测
- [x] 移除已无意义的全局 `probeOnRefresh` 开关(目标级 `probeOnRefresh: false` 保留:该目标不参与强制检测);`refreshInterval` 语义澄清为「面板检查数据的最大间隔」
- [x] 页脚改为展示「数据更新于 … + 下次检查 Ns」,tooltip 内含服务端更新节奏与快照生成时间
- [x] `scripts/verify.cjs` 适配纯外部数据源部署(自动跳过不适用的探测类断言),并新增「倒计时对齐服务端」断言
### v0.12.1 已实现
**让边缘部署真正可用 + 管理端远程访问 + 前后端分离**
起因:核实「管理端在 Cloudflare 上是否有效」时发现一个更要紧的问题 —— Worker **完全没有 UptimeRobot 同步能力**(`runChecks` 的探测目标只有 `config.monitors`)。而实际配置是 `dataFilter: uptimerobot`,也就是说**直接部署到 Worker 面板会是空的**:它探测 8 个本地条目,而真正在监控的 6 个 UR 目标它根本不认识。
- [x] **`edge/cloudflare/uptimerobot.js`(新增)**:Workers 版 UR 只读数据源,与 `prober/node/lib/uptimerobot.js` 同一套语义(术语、状态映射、ratios 解析、故障原因提取)
- [x] **`worker.js` 探测集合扩展**:自建目标 + UR 镜像目标统一纳入一轮探测;UR 目标只拉取不探测,采用其状态并补齐本地 `daily` 分桶
- [x] **`probeScope` 与 `dataFilter` 解耦**(与本地探针同语义):未显式设置 `probeScope` 时跟随 `dataFilter`;`config` 范围下不拉 UR,`uptimerobot` 范围下不探测本地
- [x] **同步失败降级**:UR 拉取失败时标记 `sources.uptimerobot.ok = false` 并保留上次数据,**绝不把目标判为离线或从面板消失**
- [x] **UR 侧删除的监控自动清理**,避免僵尸条目
- [x] 快照新增 `probeScope` 字段;UR 目标带 `meta`(面板据此区分自建/外部)与 `uptimeRatios`(面板算官方可用率)
- [x] **`settings.admin.allowRemote`**:探针**默认只监听 `127.0.0.1`**;显式打开才监听全网卡,启动日志就此告警(实测:默认时非环回地址连不上,打开后可达)。**怎么把流量送到这个端口由使用者自己决定**,项目不做任何假设
- [x] **`settings.adminUrl`(面板管理入口)**:管理界面里填一个地址,公开面板页脚就出现「管理」按钮指向它;留空则完全不显示(默认,避免在公网面板暴露管理面位置)。只接受 `http`/`https`,`javascript:` 等协议被服务端拒绝
- [x] **前后端分离落地**:Worker 侧 `/admin` 与 `/api/admin/*` 显式返回 **403 + 指向文档的提示**(而非默默落到静态资源变成 404),确保边缘侧**没有任何可写面**;管理端只在本地 Node 侧运行
- [x] **修掉一个既存的密钥泄露**:`/api/monitors` 无鉴权却回吐合并后的完整配置,**包含私有层的 UptimeRobot API Key**。现在两侧(Node 与 Worker)都对对外响应剥离 `uptimeRobot.apiKey`,保留 `enabled` 等非敏感字段供面板读取;探针内部同步仍用完整配置
- [x] **面板配置改为以服务端合并版为准**:原先只读静态 `monitors.json`(公开层),导致管理端写入私有层的设置(如 `adminUrl`)面板看不到。现在启动时额外拉 `/api/monitors` 合并 `settings`;纯静态部署没有该接口时沿用公开配置
- [x] 文档:`DEPLOYMENT.md` 补「接入 UptimeRobot 数据源」与「远程访问管理端」,各部署形态对照表加「UptimeRobot 数据源 / 管理端」两行;`SECURITY.md` 补部署形态可写能力对照、配置接口不得泄露密钥、代理层下的限流粒度损失
**测试**:`verify-worker.cjs` 扩到 40 项(新增 `[E]` 配置接口密钥泄露 4 条);`verify-security.cjs` 扩到 25 项(新增 `[D2]` 密钥泄露 5 条)。全项目 8 套 314 项全部通过。
过程中抓到两个真 bug:① UR 同步失败时降级路径引用了不含 `meta` 的旧对象,导致 `name`/`url` 为 `undefined`、UR 目标从面板上消失;② `stats.total` 按 `config.monitors.length` 计数,在 `probeScope=uptimerobot` 时与实际参与的目标数不符。
**另有一处既存漏洞**(本轮顺带修复):`/api/monitors` 从引入私有层叠加机制起就无鉴权回吐完整配置,只要在 `local.monitors.json` 里配了 `uptimeRobot.apiKey`,任何访客 `curl /api/monitors` 就能拿到。这个漏洞与本轮改动无关,是之前埋下的。
> 另有一条测试夹具层面的教训:内存版 KV 必须实现 `get(key, 'json')` 的类型参数(真实 Workers KV 会自动反序列化),否则 `state.monitors` 恒为 undefined —— 表现为「状态永远读不到」,看起来像产品代码坏了。
### v0.12.0 已实现
**① 每行绝对等高**(你的要求:不再有「内容长度相当时才等高」)
- [x] 行高与内容长度**彻底解耦**:新增 `--row-h: 72px` 作为唯一基准,表格结构上就给每一行定死高度
- [x] 名称列改为「文本区限两行 + 徽标区固定单行」两段式;文本区用 `.clamp2`(`-webkit-line-clamp: 2` + **固定 `height`**)。这里必须是 `height` 而不是 `max-height`——后者会让短内容缩成一行、长内容撑到两行,反而产生两种行高(实测 66px / 71px 混杂)
- [x] 地址列同样限两行;`延迟/结果/最近检测` 改为 `nowrap` + 省略号(这类值本就短)
- [x] 分组标题行用 `:not(.group-row)` 排除在数据行规则之外,否则会一并被套上 72px
- [x] **事件表与监控表格共用同一个 `--row-h`**,两张表在视觉上也严格对齐(此前事件表是 66px)
- [x] 完整值仍可通过悬停 `title` 查看(已加断言:所有会被截断的单元格都必须带 `title`)
- [x] **卡片视图**同样等高(grid 拉伸,13 视口 × 250px 全一致)
**② 管理 WebUI + 2FA 绑定才能进入**
- [x] 新增 `/admin` 管理后台与 `/api/admin/*` 接口;**未绑定 2FA 时除绑定流程外一律 403**,不存在绕过路径
- [x] 零依赖实现 **TOTP(RFC 6238)**:HOTP 动态截断 + Base32 编解码,**用 RFC 4226 与 RFC 6238 官方测试向量验证**(10 + 6 个向量全对)
- [x] **内置 QR 码生成器**(byte 模式 / ECC L / 版本 1–10,GF(256) Reed-Solomon + 8 种掩码惩罚评分)。用独立解码器实测:版本 1 与版本 6 的二维码都能读出**逐字节完全一致**的内容
- [x] 绑定流程四步:口令 → 生成密钥(扫码/手输/`otpauth` 链接)→ 验证 6 位码 → 展示 10 枚一次性恢复码
- [x] 绑定完成后凭本次验证码**直接换取会话**,无需再输一遍
- [x] 安全设计:会话 id 32 字节随机数且**服务端只存哈希**;`HttpOnly + SameSite=Strict` cookie;30 分钟空闲 / 2 小时最长;**同一时间步的码用过即作废**(防重放);恢复码一次性;登录失败按 IP 指数退避锁定(5 次起,1m→2m→…→30m);**不解析 `X-Forwarded-For`**
- [x] 管理功能:运行状态、探测范围、展示口径、维护窗口增删、立即重探、通知自检、目标只读列表、最近事件
- [x] **所有写操作只落 `local.monitors.json`(私有、不入库)**,绝不改 `public/monitors.json`(已加断言逐项校验)
- [x] 逃生口:用 `SHIROKO_ADMIN_SETUP_KEY` 重置 2FA
> 过程中抓到并修掉三个真 bug:① **会话 cookie 的 `Path=/admin` 覆盖不到 `/api/admin/*` 接口**,登录返回 200 但紧接着所有操作 401;② 恢复码登录被 `verifyTotp` 的 `invalid-format` 提前挡掉(恢复码是 10 位字母数字,不是 6 位数字);③ 超大请求体用 `req.destroy()` 硬断连接,客户端只看到 `fetch failed` 而拿不到状态码。
### v0.11.2 已实现
**修复你反馈的两个问题**
- [x] **「狼的评论系统」那一条高度/样式与其他行不一致**:实测在线行 **58px**、离线行 **43px**。根因是**可用率列的 90 格 mini 条**——它是 `inline-flex` 元素,参与行内布局时其基线会把行盒抬高约 15px,于是「当天有可用率数据、条正常渲染」的行比不带条的行高出一截。修复:给单元格内容统一 `vertical-align: middle`、mini 条容器改 flex 布局,现在**所有行等高(实测 43px)**
- [x] **「最近事件」改为与表格同一套样式**:不再用独立的「时间线卡片」风格,改为**与监控表格完全同构的表格**(相同的圆角容器、表头样式、行分隔线、hover、状态底色)——故障行淡红、证书行淡琥珀、恢复行不染;类型以小徽标呈现,详情列完整显示
- 实现上刻意**不共用 `.monitor-table` 类名**,避免被其他按类名统计/定位的逻辑误算(这一点被测试当场抓到:共用类名时面板条目数被多算 1~8 个)
> 说明:这一版的行高只在「内容长度相当」时保证一致;名称或地址特别长时允许换行(上一版要求的「数据完整显示」),此时行自然更高。**已在 v0.12.0 改为绝对等高**(文本列限两行 + 悬停看全文)。
### v0.11.1 已实现
**修复你反馈的三个问题**
- [x] **表格显示不全**:`table-layout: fixed` 保证了不横向滚动,但列内仍是 `nowrap`,实测**长 URL 被截掉 388px、名称截掉 219px、可用率条截掉 35px**(1440 宽下共 14 个单元格被截断)。改为**允许换行**(地址 `break-all`、名称/结果 `overflow-wrap: anywhere`)并重排列宽比例——数据完整显示且**仍然零横向滚动**(已加断言:注入超长名称/地址后截断单元格 = 0)
- [x] **「最近事件」完全没样式**:原因是上次追加 CSS 的命令是 `node --check app.js && cat >> style.css`,`--check` 报错导致 `&&` 短路,**CSS 根本没写进文件**(只有 HTML 裸奔)。这次用编辑器写入并加了断言。同时**重做为时间线样式**:左侧竖线 + 按类型着色的圆点、类型徽标、左描边分色;避免短名称后面拖出大片空白(名称列按内容自适应);**默认折叠为最近 8 条**,可「展开全部 N 条」
- [x] **`dataFilter: uptimerobot` 仍在探测本地目标**:新增 `settings.probeScope`(`all` / `config` / `uptimerobot`),**不填则跟随 `dataFilter`** —— 只展示一个来源时不再探测另一个来源的目标(省资源、少打扰);想保留探测可显式设 `"probeScope": "all"`。启动日志会明确打印当前口径与范围
**顺带修掉两个连带问题**
- [x] **UR 镜像目标此前不产生事件也不告警**:`evaluateRound` 只遍历本地配置目标,而配了 `uptimerobot` 口径时你关心的恰恰是 UR 目标 —— 等于「收不到任何提醒」。现在两类目标统一纳入事件与告警
- [x] **去抖计数被评估频率放大**:`evaluateRound` 每秒被调用,原来每次评估都让 `failStreak +1`,一个持续离线的目标 1 秒就凑够「连续 2 次失败」触发告警。改为**按探测时间戳去重**,只在真正产生新结果的那一轮累加;同时无状态变化时不再反复落盘
**测试**:`verify-views` [J] 段扩到 11 条(长内容不截断 / 仍不滚动 / 事件时间线布局 / 类型着色 / 折叠与展开),`verify-notify` 新增 [D3] 4 条(探测范围跟随、不再探测本地、显式 all 恢复探测、探测范围与展示口径解耦)与去抖计数断言。
### v0.11.0 已实现(P1 收尾)
- [x] **「最近事件」面板区块**:监控列表下方展示最近 20 条状态翻转(故障 / 恢复 / 证书 / 维护),无事件自动隐藏;窄屏隐藏「详情」列
- [x] **90 天响应时间趋势**:当日分桶累计 `latSum/latCount/latMax`,快照输出 `lat: { avg, max, days[] }`;卡片视图在可用率条下新增延迟细柱图(最近 30 天 + 90 天均值),表格视图在可用率列 tooltip 追加日均/峰值
- [x] `--demo` 同步生成 90 天延迟样例(含起伏形状),静态部署也能看到趋势效果
- [x] `verify-views` 新增 [J] 段 5 条断言(趋势条渲染 / 30 格 / tooltip / 事件空态 / 无溢出),**P0+P1 全部完成**
> 至此最初规划的 P0(通知、证书)与 P1(事件时间线、维护窗口、响应时间趋势)全部落地。
### v0.10.0 已实现(P0:通知 + 证书)
- [x] **通知渠道①:邮件** —— 自写极简 SMTP 客户端(**零依赖**,不引 nodemailer):465 隐式 TLS / 587·25 自动 STARTTLS、AUTH LOGIN 与 PLAIN、UTF-8 主题按 RFC2047 编码 + base64 正文、多收件人、5xx 与连接错误可读;服务器不支持 STARTTLS 时**默认拒绝明文发信**(需显式 `allowInsecure`)
- [x] **通知渠道②:MeoW 推送** —— 按[官方文档](https://www.chuckfang.com/MeoW/api_doc.html)实现:昵称寻址无 token、`msgType`/`htmlHeight` 走查询参数、**HTTP 恒 200 改为按响应体 status 判定**、429/404/403 转成可读原因、本地限流器遵守 3s/15min/60h
- [x] **告警逻辑**:连续 N 次失败才告警(去抖)+ 恢复通知 + 同轮多点变化合并为一条 + **去抖状态持久化**(重启不重复)+ 静默时段(跨零点,只记录不发送)
- [x] **事件时间线**:`data/events.json`(保留最近 500 条)+ `GET /api/events` + 快照内附最近 20 条;`redactDetails` 时脱敏
- [x] **TLS 证书监控**:对 https 目标单独握手取有效期(默认 6 小时一次),面板徽标(≤14 天黄 / ≤7 天红),阈值 14/7/3 各告一次、已过期只报最严重一档、换证书自动重置
- [x] **维护窗口**:窗口内不计入可用率、不发告警、显示「维护中」徽标,事件仍记录
- [x] **`--test-notify` 自检命令** 与 `--data-dir` 私有数据目录(默认 `./data`,已 gitignore)
- [x] 新增 `scripts/verify-notify.cjs`:**39 项断言**(假 SMTP 服务器 / 假 MeoW 服务器 / 静默时段 / 限流器 / 三轮集成验证去抖与恢复 / 维护窗口抑制 / 真实证书握手)
> 测试过程中抓到并修掉两个真 bug:① SMTP 空闲超时没有处理,服务器不响应会把探针循环**挂死**;② MeoW 限流器把空历史当成 0,**第一条通知永远被自己拦掉**;③ 去抖状态在「无事发生」的轮次不落盘,导致连续失败计数丢失、永远达不到告警阈值。
### v0.9.1 已实现
- [x] **「任何情况下都不需要横向滚动」定为硬要求并落地**:表格改为**流式列宽**(`table-layout: fixed` + 百分比列宽),表头允许换行、单元格裁剪,心率图与可用率条随宽度伸缩;从结构上不可能撑出横向滚动
- [x] 修复两个只有极端内容才暴露的撑破(普通数据下测不出来):
- **卡片视图**在 <768px 曾溢出最多 495px —— grid 轨道 `minmax(300px, 1fr)` + 网格项默认 `min-width: auto`,被超长 URL 撑到 801px;改为 `minmax(min(300px, 100%), 1fr)` + `.card { min-width: 0 }`
- **表格**在 <768px 曾溢出 8~42px —— 表头 `white-space: nowrap` 在窄列里溢出;改为换行 + 裁剪
- [x] 新增 `scripts/verify-layout.cjs`:自带极端配置(40 目标 / 6 分组 / 超长名称与 URL)+ 临时静态服务,扫描 **13 种视口 × 2 视图**,26 项断言零横向滚动
- [x] 被截断的名称/目标补 `title`,鼠标悬停可看完整值
### v0.9.0 已实现
**修复你反馈的两个问题**
- [x] **装饰遮罩「看不出效果」**:人物与单车改为**统一全局半透明**(`settings.decoOpacity`,默认 0.78,一个值同时作用于两者)+ 各自的边缘渐隐;此前立绘是 0.92(几乎无感)、单车 0.80,两者还不一致
- [x] **切换视图时人物比例变化**:删掉了「卡片视图把立绘缩到 300px」的规则(那是上一版为躲让卡片加的),现在两种视图尺寸完全一致
- [x] **保留原始素材**:AI 生成的白底原图归档到 `assets-src/`,处理流程写成可复现脚本 `scripts/prepare-artwork.py`(边界洪泛 + 封闭区域补抠 + 裁边缩放 + 补白)
**全局审阅发现并修复的问题**
- [x] 🔴 **无障碍对比度不达标(两套主题都中招)**:状态色直接当文字色用时远低于 WCAG AA 4.5:1(白子主题「在线」2.71:1、「等待」2.56:1;默认主题 3.30:1 / 3.19:1)。改为**双色体系**:填充色(圆点、心率条、边框)保持鲜亮并保证 ≥3:1,文字色用同色系加深值并保证 ≥4.5:1;暗色模式同步。新增 8 条对比度断言(逐层合成实际背景色,正确处理 `color-mix` 半透明底)
- [x] 🔴 **暗色模式下主题是坏的**:顶栏/卡片/表头/分组行是硬编码白色渐变,配上深色页面就是「白顶栏 + 深底」;改为随明暗模式切换的变量(新增 `docs/screenshot-dark.png`)
- [x] 🟠 **平板竖屏(768px)表格要内部横滚 242px**:自动切卡片的阈值从 700px 提到 **1024px**(= 表格实测最小舒适宽度),显式选择视图仍然优先
- [x] 🟠 **Cloudflare Worker 快照缺字段**:补 `dataUpdatedAt` / `updateIntervalSec` / `dataFilter`,边缘部署与本地探针口径一致(否则倒计时不会对齐服务端)
- [x] 🟡 清理失效 CSS(`.logo .halo`,随内联光环 SVG 移除后遗留)
**测试脚本自身修掉的两个 bug**(都会导致误报)
- 对比度取样把 `color(srgb r g b / a)` 的 0~1 分量当成 0~255、把半透明底色当不透明 → 改为逐层合成实际背景色
- 「立绘不影响阅读」把**渐变背景**(卡片用 `background-image`)误判为透明 → 判定补上背景图检查
### v0.8.3 已实现
- [x] **修复抠图漏区**:此前只用「边界洪泛」抠底,光环**环内**、单车**车架三角区与轮圈内扇形**这些被围起来的封闭白色区域没被处理,会留成白块。改为再补一次「封闭白色区域」按面积清理(人物/单车/图标三张图都处理了;脸部、袜子等服装白已确认保留)
- [x] **人物与单车加半透明 + 渐隐遮罩**:立绘 `opacity .92` + 上下渐隐,单车 `opacity .8` + 两端渐隐 + 轻微降饱和,不再像贴上去的贴纸
### v0.8.2 已实现
- [x] 左下角公路车换成**简约扁平风插画**(AI 生成 + 抠底,与人物同一套视觉语言),替换掉此前自绘的线稿 SVG;页脚下方装饰带(公路车 / 光环 / 人物)风格统一
### v0.8.1 已实现
- [x] **修复移动端顶栏塌陷**:窄屏(≤820px)改为「标题 / 统计 / 控件」三行布局,统计均分视口宽度,页脚改纵向堆叠 —— 此前统计数字会挤成竖排并压住标题
- [x] **窄屏默认卡片视图**(≤700px):表格在手机上必须横向拖动,改由卡片呈现;横竖屏切换时重算,用户显式选过视图则尊重其选择
- [x] 窄屏与横屏手机自动撤掉立绘、公路车、光环水印等固定装饰,避免压住内容或溢出
- [x] `verify-views` 新增移动端专项断言 7 项(默认卡片 / 无横向溢出 / 顶栏三行 / 统计铺满 / 装饰隐藏 / 页脚不裁切 / 无 JS 错误)
### v0.8.0 已实现
- [x] **形象换成简约风插画**(AI 生成 + 抠底,非官方美术素材):右下角全身立绘、logo 与 favicon 用同款半身像;官方立绘/头像已从仓库移除
- [x] **修复:桌面端表格需要横向拖动** —— 上一版为避让立绘给内容区加了 240px 右内边距,而 `.grid` 是 `max-width: 1200px`,内边距在 1200 内部吃掉宽度,导致 1200px 以上的屏幕也必然溢出。改为「立绘收窄 + 置于内容之下」,实测 1280 / 1440 / 1920 三种宽度下页面与表格均 **0 溢出、0 重叠**
- [x] **修复:立绘遮挡光环水印** —— 光环水印移到页脚下方空带,与左下公路车、右下立绘形成一条装饰带,互不遮挡
### v0.7.0 已实现
- [x] **砂狼白子主题**(默认启用):官方立绘(右下角,宽屏自动为它让位)+ 头像 logo/favicon(光环环呼吸动画)+ 天蓝围巾飘带顶栏 + 倒十字发饰分组标识 + 公路车轮加载态 + 按官方造型还原的倾斜椭圆光环水印;配套青绿主色调
- [x] 主题切换:`settings.theme` / `?theme=` / 顶栏光环按钮(localStorage 记忆),默认主题完整保留
- [x] `settings.artwork` 立绘位,默认指向内置立绘;未配置或文件缺失时静默不显示
- [x] 修复:`dataFilter=uptimerobot`(只展示外部数据源)时「平均延迟」恒为「–」——原规则一律排除 UR 延迟,改为「混合来源时排除、纯外部来源时以其为唯一口径」,服务端与面板同步调整
### 路线图(后续迭代)
- [ ] 通知渠道:Webhook / Server 酱 / Telegram / Bark / 邮件
- [ ] 故障事件记录:连续失败次数、事件时间线、状态变更历史
- [ ] 状态徽章:SVG badge(shields.io 风格)
- [ ] 更多探测类型:证书到期天数、响应体关键字、DNS 解析、ICMP(边缘端)、HTTP 重定向链
- [ ] 主动协议探测(Redis `PING`、MySQL 握手等):需先设计「发送探测字节」的安全边界与白名单,避免退化成通用交互工具
- [ ] 只读分享令牌:给 `hideTargets` 之外的场景做「带令牌才能看到真实名称」的分级
- [ ] 多区域探针:多 Worker 部署 + 结果合并,取容错视图
- [ ] 配置 Web UI(可选加密编辑),替代手改 JSON
- [ ] 面板:更多形态(如时间线视图、地图视图)、明暗主题手动切换、可用率区间可配置(7/30/90 天)
- [ ] 状态页 JSON API 的 CORS 开放与缓存策略细化
## 本地运行
**零依赖,不需要 `npm install`**:
```bash
git clone https://github.com/tamakyi/shiroko-monitor.git
cd shiroko-monitor
node prober/node/index.js
```
打开 (管理端在 `/admin`)。要求 Node.js ≥ 18。
改 `public/monitors.json` 里的 `monitors` 即可添加自己的目标,**文件会被自动热重载,不用重启**。
- 完整说明(三种运行模式 / 配置分层 / 开机自启 / 常见问题)→ **[docs/DEPLOY-LOCAL.md](docs/DEPLOY-LOCAL.md)**
- 面板部署到 GitHub Pages → **[docs/DEPLOY-PAGES.md](docs/DEPLOY-PAGES.md)**
## 开发
```bash
# 本地起服务(含探针)
node prober/node/index.js
# 只验证探针逻辑
node prober/node/index.js --once
# UI 自动化实测(需 playwright,仅开发验证用,驱动系统 Chrome)
NODE_PATH= node scripts/verify.cjs # 热重载 / 数据一致性 / 刷新语义 / 面板(17 项)
NODE_PATH= node scripts/verify-views.cjs # 视图与数据源过滤 / 对比度 / 等高 / 事件区块(47 项)
NODE_PATH= node scripts/verify-layout.cjs # 13 视口 × 2 视图零横向溢出 + 行高一致(26 项)
NODE_PATH= node scripts/verify-admin.cjs # 管理端 2FA 全流程(102 项:RFC 向量 / 绑定 / 会话 / 限流 / 接口 / 页面)
node scripts/verify-worker.cjs # Cloudflare Worker(40 项:UR 映射 / 探测范围 / 失败降级 / 管理端拒绝 / 密钥泄露)
node scripts/verify-security.cjs # banner 探测 / 脱敏 / 私有分层 / 刷新限流 / 密钥泄露 / CORS 白名单(40 项)
node scripts/verify-notify.cjs # 通知与证书(44 项:假 SMTP / 假 MeoW / 去抖 / 恢复 / 维护窗口 / 真实证书)
UPTIMEROBOT_API_KEY=ur... node scripts/verify-uptimerobot.cjs # UR 映射/兼容接口/失败路径/冒烟(18 项)
```
`verify-security.cjs` 会临时起一个假 SSH 服务与静默端口,验证 banner 探测的在线/不匹配/超时三种判定、默认不回显版本、脱敏与私有配置分层,以及刷新接口的 429/403 行为。`verify-uptimerobot.cjs` 用合成 payload 做无网映射单测,真实 Key 冒烟为可选(未设置环境变量则跳过)。`verify-notify.cjs` 会临时起**假 SMTP 服务器与假 MeoW 服务器**,完整走一遍邮件握手与推送协议,并真跑三轮探针验证去抖、恢复、维护窗口抑制与事件落盘;真实证书握手需要外网,无网络时自动跳过(不计失败)。`verify-layout.cjs` 用极端内容(40 目标 / 6 分组 / 超长名称与 URL)在 13 种视口 × 2 种视图下断言零横向溢出**且行高完全一致**。`verify-admin.cjs` 先用 RFC 4226 / RFC 6238 官方测试向量校验自实现的 TOTP,再覆盖 2FA 绑定、会话生命周期、防重放、失败限流、HTTP 鉴权边界与页面流程。`verify-worker.cjs` 把 Worker 以 data: URL 动态载入(绕开 `cloudflare:sockets`),注入假的 KV / 静态资源 / UptimeRobot 服务器,**不需要 wrangler 也不需要联网**即可验证边缘逻辑。
代码约定:探针与面板通过 `status.json` / `/api/status` 的同一份 JSON 契约协作;新增探测类型时只需在 `prober/node/lib/` 与 `edge/cloudflare/worker.js` 各实现一个 `probeXxx(monitor)`,返回 `{ status, latency, code, detail }` 即可被面板直接渲染。
## 界面截图
| 砂狼白子主题(默认,表格视图) | 默认主题 |
| --- | --- |
|  |  |
| 深色模式(白子主题) | 卡片视图(白子主题) |
| --- | --- |
|  |  |
移动端(390×844,自动切卡片 + 顶栏三行):`docs/screenshot-mobile.png`
UptimeRobot 数据源(`?filter=uptimerobot`,截图中含真实监控名与域名,公开发布前可自行删除):`docs/screenshot-uptimerobot.png`;演示数据 90 天满档:`docs/screenshot-demo.png`
## 致谢与定位
交互设计参考 Uptime Kuma(心跳图、90 天可用率条、分组卡片、汇总统计),实现为自研轻量版:不追求 Kuma 的通知/多用户/数据库体系,只保留「配置文件 → 探针 → 状态面板」的最小闭环,并对静态/边缘部署友好。