Files
shiroko-monitor/README.md
T
tamakyi ea846dee31 fix(layout): 表格流式列宽,任何内容与视口都不再需要横向滚动
硬要求(用户明确):不允许出现需要滚动才能看全的情况。

改动:
- 表格 table-layout: fixed + 百分比列宽(8/16/21/8/7/11/17/12%),
  表头允许换行并裁剪、单元格裁剪、心率图与可用率条随宽度伸缩
- 卡片网格 minmax(min(300px,100%),1fr) + .card{min-width:0}
  (修 grid 的 min-width:auto 撑破:320px 下曾溢出 495px)
- 截断的名称/目标补 title,悬停可看完整值
- 新增 scripts/verify-layout.cjs:自带极端配置(40 目标 / 6 分组 /
  超长名称与 URL)+ 临时静态服务,扫描 13 视口 × 2 视图,26 项断言零横向滚动

验证:五套 115 项全通过(17+30+26+20+22)
2026-10-13 22:37:02 +08:00

557 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
启动后打开 <http://localhost:8787/> 即可看到面板。无需安装任何依赖。
## 项目结构
```
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
│ └── 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(详见「面板形态与数据源」)
"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 只保留「正常/异常」
"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.<id>.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=<path>` 写到别处
## 部署方案
| 方案 | 架构 | 数据更新 | 成本 | 适合场景 |
| --- | --- | --- | --- | --- |
| 本地 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=<path>` 自定义快照输出路径;`--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.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 开放与缓存策略细化
## 开发
```bash
# 本地起服务(含探针)
node prober/node/index.js
# 只验证探针逻辑
node prober/node/index.js --once
# UI 自动化实测(需 playwright,仅开发验证用,驱动系统 Chrome)
NODE_PATH=<playwright 的 node_modules> node scripts/verify.cjs # 热重载 / 数据一致性 / 刷新语义 / 面板(21 项)
NODE_PATH=<playwright 的 node_modules> node scripts/verify-views.cjs # 视图与数据源过滤(10 项)
node scripts/verify-security.cjs # banner 探测 / 脱敏 / 私有分层 / 刷新限流(20 项)
UPTIMEROBOT_API_KEY=ur... node scripts/verify-uptimerobot.cjs # UR 映射/兼容接口/失败路径/冒烟(22 项)
```
`verify-security.cjs` 会临时起一个假 SSH 服务与静默端口,验证 banner 探测的在线/不匹配/超时三种判定、默认不回显版本、脱敏与私有配置分层,以及刷新接口的 429/403 行为。`verify-uptimerobot.cjs` 用合成 payload 做无网映射单测,真实 Key 冒烟为可选(未设置环境变量则跳过)。
代码约定:探针与面板通过 `status.json` / `/api/status` 的同一份 JSON 契约协作;新增探测类型时只需在 `prober/node/lib/` 与 `edge/cloudflare/worker.js` 各实现一个 `probeXxx(monitor)`,返回 `{ status, latency, code, detail }` 即可被面板直接渲染。
## 界面截图
| 砂狼白子主题(默认,表格视图) | 默认主题 |
| --- | --- |
| ![shiroko](docs/screenshot-desktop.png) | ![default](docs/screenshot-theme-default.png) |
| 深色模式(白子主题) | 卡片视图(白子主题) |
| --- | --- |
| ![dark](docs/screenshot-dark.png) | ![cards](docs/screenshot-view-cards.png) |
移动端(390×844,自动切卡片 + 顶栏三行):`docs/screenshot-mobile.png`
UptimeRobot 数据源(`?filter=uptimerobot`,截图中含真实监控名与域名,公开发布前可自行删除):`docs/screenshot-uptimerobot.png`;演示数据 90 天满档:`docs/screenshot-demo.png`
## 致谢与定位
交互设计参考 Uptime Kuma(心跳图、90 天可用率条、分组卡片、汇总统计),实现为自研轻量版:不追求 Kuma 的通知/多用户/数据库体系,只保留「配置文件 → 探针 → 状态面板」的最小闭环,并对静态/边缘部署友好。