fix(ui): 修事件表四列文字错位 + 跨域隐藏刷新按钮 + 补 Pages 部署文档
① 事件表「故障」「狼的评论系统」「UptimeRobot:403 Forbidden」不在同一行 根因(实测定位,非推断):四个 td 的 top/height 完全相同(696/72),错位在单元格内部。 .clamp2 为保证行等高固定了 height:2.8em(36px),而「故障」徽标只有 17px; 单元格按 vertical-align:middle 对齐时,两者居中基准不同,徽标比其它三列低约 9px。 修法:事件表单元格改顶部对齐 + clamp2 在事件表内改为 max-height。 实测修复后四列文字顶端错位 0px。 ② 跨域模式隐藏刷新按钮 /api/refresh 在跨域下必然被 refreshLocalOnly 挡下(对端是代理地址而非私网), 给一个必然失败的按钮不如隐藏。setSource 里按 source==='remote' 切换, 同源模式保留(并补断言守住这个行为)。 ③ 新增 docs/DEPLOY-PAGES.md GitHub Pages 面板 + 本地探针的完整部署指南:架构图、先决条件(含混合内容硬限制)、 本地探针启动与 2FA 绑定、Pages 配置三处填法、验证清单、安全要点、 Actions 快照备选方案、六条常见问题排查(Mixed Content / CORS / Failed to fetch 等)。 测试:verify-views 47→49 项(事件行文字顶端对齐 0px、同源保留刷新按钮)。 全项目 8 套 336 项通过。
@@ -524,6 +524,8 @@ node prober/node/index.js
|
||||
|
||||
### 面板部署在别处(跨域只读)
|
||||
|
||||
> 完整步骤见 **[docs/DEPLOY-PAGES.md](docs/DEPLOY-PAGES.md)**(GitHub Pages 面板 + 本地探针的部署指南,含常见问题排查)
|
||||
|
||||
面板与探针可以放在两台机器上:面板放 GitHub Pages / Cloudflare(静态托管),探针放你家里或内网的机器上持续探测。两项配置:
|
||||
|
||||
| 配置 | 作用 |
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# 部署指南:GitHub Pages 面板 + 本地探针
|
||||
|
||||
面向的场景:**面板托管在 GitHub Pages(公开、免运维),探测跑在自己家里/内网的机器上**。
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐ ┌──────────────────────────────────┐
|
||||
│ GitHub Pages(静态托管) │ HTTPS │ 你自己的机器(内网/家用) │
|
||||
│ ├─ index.html + assets/ │◄────────┤ ├─ Node 探针:持续探测 │
|
||||
│ └─ 读 status.json 取配置 │ fetch │ ├─ /admin 管理端(2FA 保护) │
|
||||
│ │ │ └─ 写 local.monitors.json │
|
||||
└─────────────────────────────┘ └──────────────────────────────────┘
|
||||
```
|
||||
|
||||
为什么面板不能直接读本地探针?因为 GitHub Pages 是**纯静态**的,只能读仓库里的文件。
|
||||
数据要上去,靠两件事:**跨域读接口**(本指南的方式)或 **Actions 提交快照**(下文备选方案)。
|
||||
|
||||
---
|
||||
|
||||
## 一、先决条件
|
||||
|
||||
| 条件 | 说明 |
|
||||
| --- | --- |
|
||||
| **本地探针有 HTTPS 地址** | Pages 强制 HTTPS,浏览器禁止 HTTPS 页面请求 HTTP(混合内容)。用你自己的隧道 / 反代给探针套一层 HTTPS |
|
||||
| Node.js ≥ 18| 零依赖,只需 Node 自带模块 |
|
||||
| 一个 GitHub 仓库 | 公开仓库即可(Pages 免费额度对公开仓库无限) |
|
||||
|
||||
> ⚠️ **混合内容是硬限制**:即使 CORS 配对了,只要探针是 `http://`,浏览器仍会直接拦截请求,控制台只留一行 `Mixed Content ... was blocked`。这一条没有绕过办法。
|
||||
|
||||
---
|
||||
|
||||
## 二、本地:跑探针
|
||||
|
||||
```bash
|
||||
git clone <你的仓库>
|
||||
cd shiroko-monitor
|
||||
|
||||
# 私有配置(不入库):真实地址、API Key 等放这里
|
||||
cp local.monitors.json.example local.monitors.json # 或按下面提示自建
|
||||
node prober/node/index.js
|
||||
```
|
||||
|
||||
启动后确认:
|
||||
|
||||
```
|
||||
面板:http://localhost:8787/
|
||||
管理后台: http://localhost:8787/admin
|
||||
```
|
||||
|
||||
### 让探针能被外部访问
|
||||
|
||||
默认它**只监听 `127.0.0.1`**。要让隧道/反代连进来,在 `local.monitors.json` 里打开:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"settings": {
|
||||
"admin": { "allowRemote": true } // 监听 0.0.0.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 打开后同一端口上的公开面板也会对外可达(只读,但目标名称/地址/可用率会暴露)。
|
||||
> 如果不希望这样,请在隧道那层按路径只放行 `/admin` 与 `/api/admin/*`。
|
||||
|
||||
### 绑定管理端 2FA(必做)
|
||||
|
||||
管理端能改配置、发通知,必须先绑定两步验证:
|
||||
|
||||
```bash
|
||||
# 首次绑定需要这个口令(只从环境变量读,不写进任何文件)
|
||||
export SHIROKO_ADMIN_SETUP_KEY='换成一串足够长的随机口令'
|
||||
node prober/node/index.js
|
||||
```
|
||||
|
||||
然后打开 `https://你的隧道地址/admin`,按提示扫码绑定、保存恢复码。
|
||||
|
||||
---
|
||||
|
||||
## 三、页面:部署到 GitHub Pages
|
||||
|
||||
### 1. 开启 Pages
|
||||
|
||||
仓库 **Settings → Pages → Build and deployment → Source** 选 **GitHub Actions**。
|
||||
|
||||
### 2. 推送代码
|
||||
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. 填两处配置(关键)
|
||||
|
||||
打开 `https://你的隧道地址/admin`,在「口径与范围」区块填:
|
||||
|
||||
| 字段 | 填什么 |
|
||||
| --- | --- |
|
||||
| **面板上的管理入口链接** | `https://你的隧道地址/admin` —— Pages 面板页脚会出现「管理」按钮跳过去 |
|
||||
| **跨域数据源地址** | `https://你的隧道地址`(不带路径)—— 面板从这里取数据 |
|
||||
| **允许跨域读取的页面地址** | `https://你的用户名.github.io` —— 必须是完整来源,含协议 |
|
||||
|
||||
填完立刻生效。回本地仓库提交改动:
|
||||
|
||||
```bash
|
||||
git add public/monitors.json && git commit -m "chore: 配置跨域数据源" && git push
|
||||
```
|
||||
|
||||
> 配置写在 `local.monitors.json`(私有层)的话,Pages 上的面板读不到 ——
|
||||
> 它只能读仓库里的 `public/monitors.json`。所以上面的两步(管理界面填 → 提交仓库)都要做。
|
||||
|
||||
### 4. 验证
|
||||
|
||||
打开 `https://你的用户名.github.io/<仓库名>/`,检查:
|
||||
|
||||
- 页脚数据源显示「**远程探针(跨域)**」—— 说明跨域打通了
|
||||
- 表格有数据且状态正常
|
||||
- 页脚有「管理」按钮,点击能跳到管理端
|
||||
- **页面上没有「刷新」按钮** —— 跨域模式下该接口必然被`refreshLocalOnly` 挡下,隐藏比给一个失败的按钮好
|
||||
|
||||
---
|
||||
|
||||
## 四、安全要点
|
||||
|
||||
| 事项 | 说明 |
|
||||
| --- | --- |
|
||||
| **管理端 2FA 必须绑定** | TOTP(Google Authenticator 等)+ 10 枚一次性恢复码 |
|
||||
| **恢复码离线保存** | 只在绑定时显示一次,用掉一枚少一枚 |
|
||||
| **跨域白名单不要图省事写 `*`** | 代码里不存在 `*` 分支,写了也不生效。必须写完整来源,如 `https://you.github.io` |
|
||||
| **管理端永不放行跨域** | `/admin` 与 `/api/admin/*` 不带任何 `Access-Control-*` 头,外部页面既读不到也改不了 |
|
||||
| **会话** | 30 分钟无操作失效,最长 2 小时;进程重启全部失效 |
|
||||
| **登录限流** | 同一 IP 连续 5 次失败后指数退避锁定 |
|
||||
| **API Key** | 放 `local.monitors.json` 或环境变量,**绝不要写进 `public/monitors.json`**(会随Pages 公开) |
|
||||
|
||||
---
|
||||
|
||||
## 五、备选:不用跨域,改用 Actions 提交快照
|
||||
|
||||
如果不想暴露探针的 HTTPS 地址,可以让数据「流」到 Pages 而不是「拉」:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/monitor.yml(示意)
|
||||
on:
|
||||
schedule: [{ cron: '*/5 * * * *' }]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
probe:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: node prober/node/index.js --once --config=public/monitors.json
|
||||
- run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git add public/status.json
|
||||
git diff --staged --quiet || git commit -m "chore: status" && git push
|
||||
```
|
||||
|
||||
面板用 `?source=static` 读仓库里的 `status.json`。
|
||||
|
||||
**取舍**:探测跑在 GitHub Runner 上(不是你的机器),延迟 ≤5 分钟,且**没有通知、维护窗口、事件、2FA 管理端**。适合纯展示需求。
|
||||
|
||||
---
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
**面板一直转圈 / 报错**
|
||||
浏览器控制台看具体报错:
|
||||
- `Mixed Content ... was blocked` → 探针地址不是 HTTPS,回第一节
|
||||
- `blocked by CORS policy` → 「允许跨域读取的页面地址」没填对,或与实际访问地址不一致(含 `www`、端口、大小写)
|
||||
- `Failed to fetch` 且数据源显示「浏览器直连」 → 面板拿不到数据,退到了兜底模式
|
||||
|
||||
**页脚没有「管理」按钮**
|
||||
「面板上的管理入口链接」为空(默认就是空的,为了不在公网暴露管理面位置)。
|
||||
|
||||
**跨域下没有刷新按钮**
|
||||
这是预期行为。跨域模式下 `/api/refresh` 会被服务端的 `refreshLocalOnly` 挡下(对端是代理地址而非私网),前端自动隐藏了这个按钮。
|
||||
|
||||
**改了配置但面板没变**
|
||||
管理端写的是 `local.monitors.json`(私有层)。如果那部分设置需要 Pages 上的面板看到,得手动同步到 `public/monitors.json` 并提交。
|
||||
|
||||
**本机开发时前端跨域被拒**
|
||||
默认白名单放行 `localhost` / `127.0.0.1`且**忽略端口**,所以 `localhost:5173` 这类能直接用。若你在局域网 IP 上跑前端,需把那个地址显式加进白名单。
|
||||
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 452 KiB After Width: | Height: | Size: 452 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 214 KiB |
@@ -85,6 +85,16 @@ async function mapLimit(items, limit, fn) {
|
||||
function setSource(s) {
|
||||
state.source = s;
|
||||
$('source-badge').textContent = `数据源:${SOURCE_LABEL[s] ?? s}`;
|
||||
// 跨域模式下「刷新」没有意义:/api/refresh 会被服务端的 refreshLocalOnly 挡住
|
||||
// (对端是代理地址而非私网),前端也会退回只读。与其给一个必然失败的按钮,不如隐藏。
|
||||
const crossOrigin = s === 'remote';
|
||||
const btn = $('refresh-btn');
|
||||
if (btn) {
|
||||
btn.hidden = crossOrigin;
|
||||
btn.title = crossOrigin ? '' : '立即重新跑一轮探测(需要服务端允许)';
|
||||
}
|
||||
const ctrl = document.querySelector('.controls');
|
||||
if (ctrl) ctrl.classList.toggle('no-refresh', crossOrigin);
|
||||
}
|
||||
|
||||
// 跨域数据源:settings.dataUrl 指向另一台机器上的探针(通常是本机,但页面部署在别处)。
|
||||
|
||||
@@ -176,6 +176,8 @@ body {
|
||||
.view-btn + .view-btn { border-left: 1px solid var(--border); }
|
||||
.view-btn:hover { color: var(--text); }
|
||||
.view-btn.active { background: var(--accent); color: #fff; }
|
||||
/* 跨域模式下刷新按钮被隐藏(见 app.js setSource),此时控件区只剩视图切换 */
|
||||
.controls.no-refresh { gap: 0; }
|
||||
|
||||
/* ---------- 分组页签 ---------- */
|
||||
.group-tabs {
|
||||
@@ -549,6 +551,13 @@ tr[data-status="pending"] .ev-badge { background: var(--pending-bg); color: var(
|
||||
.ev-maint { margin-left: 6px; border-radius: 999px; padding: 0 6px; font-size: 10px; font-weight: 650; background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
|
||||
.ev-name { font-weight: 600; }
|
||||
.ev-detail { color: var(--text-2); }
|
||||
/* 事件表:单元格一律顶部对齐。clamp2 为了保证等高固定了 height:2.8em,
|
||||
若单元格按 middle 对齐,17px 的徽标与 36px 的文本框居中基准不同,
|
||||
同一行里「类型」会比「时间/目标/详情」低约 9px(实测截图确认)。 */
|
||||
.events-table td { vertical-align: top; }
|
||||
.events-table td > * { vertical-align: top; }
|
||||
/* 事件行的行高已由 --row-h 固定,clamp2 可以只做上限不必占满两行 */
|
||||
.events-table .clamp2 { height: auto; max-height: 2.8em; }
|
||||
/* 事件表的长文本同样限两行,保证与监控表格一致的等高行 */
|
||||
.ev-name, .ev-detail { white-space: normal; }
|
||||
.ev-name > .clamp2, .ev-detail > .clamp2 { overflow-wrap: anywhere; }
|
||||
|
||||
@@ -413,6 +413,29 @@ async function main() {
|
||||
return { down: down ? getComputedStyle(down).backgroundColor : '无', up: up ? getComputedStyle(up).backgroundColor : '无', downCells: down ? down.querySelectorAll('td').length : 0 };
|
||||
});
|
||||
check('事件行复用表格状态底色(故障行着色、恢复行不染)', /^(color\(|rgba?\(\d)/.test(evBg.down) && evBg.down !== 'rgba(0, 0, 0, 0)' && evBg.up === 'rgba(0, 0, 0, 0)' && evBg.downCells === 4, `故障=${evBg.down.slice(0, 30)} 恢复=${evBg.up} 列数=${evBg.downCells}`);
|
||||
// 事件行的四列文字必须落在同一水平线上。此前 clamp2 固定 height:2.8em(36px)、
|
||||
// 而徽标只有 17px,单元格按 middle 对齐时两者居中基准不同,
|
||||
// 「类型」会比「时间/目标/详情」低约 9px(实测截图确认)。
|
||||
const evAlign = await dp.evaluate(() => {
|
||||
const rows = [...document.querySelectorAll('.events-table tbody tr')];
|
||||
const spreads = [];
|
||||
for (const tr of rows) {
|
||||
const tops = [];
|
||||
for (const td of tr.querySelectorAll('td')) {
|
||||
// Range 要用独立实例:selectNodeContents 返回的是布尔值,不是 Range
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(td);
|
||||
const rects = [...range.getClientRects()];
|
||||
if (rects.length) tops.push(Math.round(rects[0].top));
|
||||
}
|
||||
if (tops.length) spreads.push(Math.max(...tops) - Math.min(...tops));
|
||||
}
|
||||
return { worst: spreads.length ? Math.max(...spreads) : -1, rows: spreads.length };
|
||||
});
|
||||
check('事件行各列文字顶端对齐(徽标与文本同一水平线)', evAlign.worst === 0, `最大错位 ${evAlign.worst}px(${evAlign.rows} 行)`);
|
||||
// 跨域模式下隐藏刷新按钮(该模式下 /api/refresh 必然被 refreshLocalOnly 挡下)
|
||||
const refreshBtnShown = await dp.evaluate(() => !document.getElementById('refresh-btn').hidden);
|
||||
check('同源模式下保留刷新按钮', refreshBtnShown === true, `可见=${refreshBtnShown}`);
|
||||
check('事件默认折叠为 8 条并可展开', evr.shown === 8 && evr.moreHidden === false && /展开全部 12 条/.test(evr.moreText), `显示 ${evr.shown} 条,按钮「${evr.moreText}」,计数「${evr.count}」`);
|
||||
await dp.click('#events-more');
|
||||
await dp.waitForTimeout(300);
|
||||
|
||||