diff --git a/README.md b/README.md
index a4ebf8b..803c23c 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,9 @@ shiroko-monitor/
│ ├── verify.cjs # 热重载 / 数据一致性 / 刷新语义 / 面板渲染
│ ├── verify-views.cjs # 视图渲染、切换持久化与数据源过滤
│ ├── verify-security.cjs # banner 探测 / 脱敏 / 私有分层 / 刷新限流
-│ └── verify-uptimerobot.cjs # UR 映射单测 / 兼容接口 / 失败路径 / 真实 Key 冒烟
+│ ├── verify-uptimerobot.cjs # UR 映射单测 / 兼容接口 / 失败路径 / 真实 Key 冒烟
+│ └── prepare-artwork.py # 从 assets-src/ 原图生成透明素材(边界 + 封闭区域两步抠图)
+├── assets-src/ # 插画原始素材(AI 生成的白底原图,供重新处理)
├── local.monitors.json # 私有配置(不入库):真实地址、API Key
├── edge/cloudflare/ # Cloudflare Workers 边缘探针
│ ├── worker.js # cron 探针 + KV 存储 + /api/* + 静态伺服
@@ -106,7 +108,8 @@ shiroko-monitor/
"description": "轻量级服务监控面板",
"view": "table", // 默认视图:table | cards
"theme": "shiroko", // shiroko(砂狼白子)| default
- "artwork": "", // 可选:立绘图片路径(相对 public/),如 assets/character.png
+ "artwork": "", // 立绘图片路径(相对 public/),留空则不显示
+ "decoOpacity": 0.78, // 人物/单车装饰的全局透明度(0~1,一个值同时作用于两者)
"dataFilter": "both", // both | config | uptimerobot(详见「面板形态与数据源」)
"refreshInterval": 30, // 面板检查数据的最大间隔(秒,最小 5);实际对齐服务端更新节奏
"dataSource": "auto", // auto | api | static | browser
@@ -262,7 +265,7 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自
指定方式(优先级从高到低):URL 参数 `?view=table|cards` → 浏览器上次选择(localStorage)→ `settings.view` → 表格。
-**移动端**:窄屏(≤700px)在用户没有显式选择过视图时**自动使用卡片**(表格在手机上必须横向拖动,体验差),并在横竖屏切换时重算;顶栏在 ≤820px 改为「标题 / 统计 / 控件」三行,立绘与固定装饰自动撤掉。
+**移动端与窄窗口**:视口窄于 **1024px**(表格含心率图与可用率条,实测最小舒适宽度约 1010px)且用户没有显式选择过视图时,**自动使用卡片**;横竖屏切换时重算,显式选择(`?view=` 或点过切换器)始终优先。顶栏在 ≤820px 改为「标题 / 统计 / 控件」三行,立绘与固定装饰自动撤掉。
### 数据源过滤
@@ -304,13 +307,13 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自
| 位置 | 元素 | 取自设定 |
| --- | --- | --- |
| 左上 logo / 浏览器图标 | **简约风半身头像**(光环 + 狼耳 + 围巾)+ 青色光环环(呼吸式外扩动画) | 生成插画的半身裁切 |
-| 右下角 | **简约风全身立绘**(透明底,约 44vh,窄身设计:实测 1280 / 1440 / 1920 下都不遮挡表格,页面也无横向滚动);半透明 + 上下渐隐,融入背景 | 生成插画 |
+| 右下角 | **简约风全身立绘**(透明底,窄身设计:实测 1280 / 1440 / 1920 下都不遮挡表格,页面也无横向滚动);全局半透明(`settings.decoOpacity`,默认 0.78)+ 上下渐隐,融入背景 | 生成插画 |
| 顶栏底边 | **天蓝色围巾飘带**:波浪形状 + 5 处褶皱纹路(自绘) | 天蓝色围巾是星野送给她的信物 |
| 页脚下方 | **光环水印**(自绘:倾斜椭圆环 + 四方位带圆头的短杆) | 白子的光环是倾斜的椭圆环,四个方位各有一条带圆头的短杆,整体形似全息瞄准镜准星 |
| 状态点 | **迷你光环**(外圈 + 中心点,在线时缓慢脉冲外扩) | 光环是基沃托斯学生的标志 |
| 分组标题 | **青色倒十字发饰**(旋转 45°,自绘) | 她左侧刘海上别的青色十字发夹 |
| 刷新按钮加载态 | **公路车轮**(轮圈 + 辐条 + 花鼓)旋转 | 她最爱的公路车(Bianchi ARIA 原型) |
-| 左下角 | 公路车插画(简约扁平风,与人物同一套视觉语言;半透明 + 两端渐隐) | 她最爱的公路车(Bianchi ARIA 原型) |
+| 左下角 | 公路车插画(简约扁平风,与人物同一套视觉语言;与人物共用同一个全局透明度 `decoOpacity` + 两端渐隐) | 她最爱的公路车(Bianchi ARIA 原型) |
| 配色 | 天蓝 / 青 + 藏蓝 + 灰白;「正常」色改为青绿(`#12b0a2`) | 阿拜多斯藏蓝校服 + 天蓝围巾 + 灰白发色 |
左下公路车、页脚光环、右下立绘构成页脚下方的一条装饰带,三者互不遮挡。
@@ -441,6 +444,27 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自
- [x] 页脚改为展示「数据更新于 … + 下次检查 Ns」,tooltip 内含服务端更新节奏与快照生成时间
- [x] `scripts/verify.cjs` 适配纯外部数据源部署(自动跳过不适用的探测类断言),并新增「倒计时对齐服务端」断言
+### 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] **修复抠图漏区**:此前只用「边界洪泛」抠底,光环**环内**、单车**车架三角区与轮圈内扇形**这些被围起来的封闭白色区域没被处理,会留成白块。改为再补一次「封闭白色区域」按面积清理(人物/单车/图标三张图都处理了;脸部、袜子等服装白已确认保留)
@@ -509,9 +533,11 @@ UPTIMEROBOT_API_KEY=ur... node scripts/verify-uptimerobot.cjs # UR 映
| --- | --- |
|  |  |
-| 卡片视图(白子主题) | 移动端 |
+| 深色模式(白子主题) | 卡片视图(白子主题) |
| --- | --- |
-|  |  |
+|  |  |
+
+移动端(390×844,自动切卡片 + 顶栏三行):`docs/screenshot-mobile.png`
UptimeRobot 数据源(`?filter=uptimerobot`,截图中含真实监控名与域名,公开发布前可自行删除):`docs/screenshot-uptimerobot.png`;演示数据 90 天满档:`docs/screenshot-demo.png`
diff --git a/assets-src/bike-original.png b/assets-src/bike-original.png
new file mode 100644
index 0000000..7b90684
Binary files /dev/null and b/assets-src/bike-original.png differ
diff --git a/assets-src/figure-original.png b/assets-src/figure-original.png
new file mode 100644
index 0000000..aa8fd31
Binary files /dev/null and b/assets-src/figure-original.png differ
diff --git a/assets-src/icon-original.png b/assets-src/icon-original.png
new file mode 100644
index 0000000..be585cf
Binary files /dev/null and b/assets-src/icon-original.png differ
diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png
new file mode 100644
index 0000000..c3b7414
Binary files /dev/null and b/docs/screenshot-dark.png differ
diff --git a/docs/screenshot-demo.png b/docs/screenshot-demo.png
index 4ac0dd6..8b833c8 100644
Binary files a/docs/screenshot-demo.png and b/docs/screenshot-demo.png differ
diff --git a/docs/screenshot-desktop.png b/docs/screenshot-desktop.png
index d52c142..4a8c15c 100644
Binary files a/docs/screenshot-desktop.png and b/docs/screenshot-desktop.png differ
diff --git a/docs/screenshot-mobile.png b/docs/screenshot-mobile.png
index 8982a80..7ef886a 100644
Binary files a/docs/screenshot-mobile.png and b/docs/screenshot-mobile.png differ
diff --git a/docs/screenshot-shiroko.png b/docs/screenshot-shiroko.png
index 0d8fc9b..019d813 100644
Binary files a/docs/screenshot-shiroko.png and b/docs/screenshot-shiroko.png differ
diff --git a/docs/screenshot-theme-default.png b/docs/screenshot-theme-default.png
index 0195a68..afcc04c 100644
Binary files a/docs/screenshot-theme-default.png and b/docs/screenshot-theme-default.png differ
diff --git a/docs/screenshot-view-cards.png b/docs/screenshot-view-cards.png
index 4e90192..8187c6a 100644
Binary files a/docs/screenshot-view-cards.png and b/docs/screenshot-view-cards.png differ
diff --git a/docs/screenshot-view-table.png b/docs/screenshot-view-table.png
index f0bf936..9439217 100644
Binary files a/docs/screenshot-view-table.png and b/docs/screenshot-view-table.png differ
diff --git a/edge/cloudflare/worker.js b/edge/cloudflare/worker.js
index 2f78a67..742de04 100644
--- a/edge/cloudflare/worker.js
+++ b/edge/cloudflare/worker.js
@@ -11,7 +11,7 @@
import { connect } from 'cloudflare:sockets';
-const VERSION = '0.8.3';
+const VERSION = '0.9.0';
const HISTORY_LIMIT = 48;
const UPTIME_DAYS = 90;
const USER_AGENT = 'shiroko-monitor/0.3 (lightweight uptime panel)';
@@ -202,10 +202,25 @@ async function runChecks(env) {
}
}
+ // 与本地探针保持同一份快照口径:dataUpdatedAt / updateIntervalSec / dataFilter
+ // 面板据此把倒计时对齐服务端节奏,并按同一过滤口径展示
+ const filter = ['both', 'config', 'uptimerobot'].includes(config.settings?.dataFilter) ? config.settings.dataFilter : 'both';
+ const intervals = [];
+ if (filter !== 'uptimerobot') {
+ for (const m of config.monitors ?? []) {
+ const n = Number(m.interval);
+ if (Number.isFinite(n) && n > 0) intervals.push(n);
+ }
+ }
+
const next = {
prober: `shiroko-monitor-worker/${VERSION}`,
demo: false,
generatedAt: new Date().toISOString(),
+ // state.generatedAt 是上一轮探测完成的时间,即「数据最后变更时刻」
+ dataUpdatedAt: state.generatedAt ?? new Date().toISOString(),
+ updateIntervalSec: Math.max(5, Math.round(intervals.length ? Math.min(...intervals) : 60)),
+ dataFilter: filter,
stats: {
total: (config.monitors ?? []).length,
up,
@@ -214,6 +229,7 @@ async function runChecks(env) {
avgLatency: latencyCount ? Math.round(latencySum / latencyCount) : null,
avgUptime90: uptimeTotal ? Math.round((uptimeUp / uptimeTotal) * 1000) / 10 : null,
},
+ sources: {},
monitors,
};
await env.MONITOR_KV.put('state', JSON.stringify(next));
diff --git a/prober/node/index.js b/prober/node/index.js
index 8ee504d..ea8a248 100644
--- a/prober/node/index.js
+++ b/prober/node/index.js
@@ -35,7 +35,7 @@ const CONCURRENCY = 8;
const WRITE_DEBOUNCE_MS = 300;
const DEFAULT_INTERVAL_SEC = 60;
const PORT = Number(process.env.PORT || 8787);
-const VERSION = '0.8.3';
+const VERSION = '0.9.0';
const UR_DEFAULT_POLL_SEC = 300;
const UR_MIN_REFRESH_GAP_MS = 60000;
diff --git a/public/assets/app.js b/public/assets/app.js
index 09190a2..f4f76b8 100644
--- a/public/assets/app.js
+++ b/public/assets/app.js
@@ -1,4 +1,4 @@
-const VERSION = '0.8.3';
+const VERSION = '0.9.0';
const HB_LIMIT = 48;
const UPTIME_DAYS = 90;
const BROWSER_CONCURRENCY = 6;
@@ -507,9 +507,12 @@ function renderViewSwitch() {
}
}
-// 窄屏(手机)判定:表格在最窄的屏幕上必须横向拖动,不如默认给卡片视图
+// 表格在窄屏必须横向滚动。实测表格最小舒适宽度约 1010px(列含心率图与可用率条),
+// 因此视口窄于 1024px 时默认落到卡片;用户显式选择过视图则以选择为准。
+const TABLE_MIN_WIDTH = 1024;
+
function isNarrowScreen() {
- return window.matchMedia('(max-width: 700px)').matches;
+ return window.innerWidth < TABLE_MIN_WIDTH;
}
function resolveView() {
@@ -792,6 +795,9 @@ function applyTheme(theme, { persist = true } = {}) {
state.theme = THEMES[theme] ? theme : 'default';
if (state.theme === 'default') delete document.body.dataset.theme;
else document.body.dataset.theme = state.theme;
+ // 装饰(人物/单车)的全局透明度:一个值同时作用于两者,避免其中一个更抢眼
+ const decoOpacity = Number(state.config?.settings?.decoOpacity);
+ document.body.style.setProperty('--sh-deco-opacity', String(Number.isFinite(decoOpacity) && decoOpacity > 0 && decoOpacity <= 1 ? decoOpacity : 0.78));
if (persist) {
try {
localStorage.setItem('shiroko.theme', state.theme);
diff --git a/public/assets/favicon-shiroko.png b/public/assets/favicon-shiroko.png
index 6f7441a..57ef45b 100644
Binary files a/public/assets/favicon-shiroko.png and b/public/assets/favicon-shiroko.png differ
diff --git a/public/assets/shiroko-bike.webp b/public/assets/shiroko-bike.webp
index 08b10c0..ec41a33 100644
Binary files a/public/assets/shiroko-bike.webp and b/public/assets/shiroko-bike.webp differ
diff --git a/public/assets/shiroko-figure.webp b/public/assets/shiroko-figure.webp
index 6a9a51d..1f0b89b 100644
Binary files a/public/assets/shiroko-figure.webp and b/public/assets/shiroko-figure.webp differ
diff --git a/public/assets/shiroko-icon.webp b/public/assets/shiroko-icon.webp
index 844c944..50ceb10 100644
Binary files a/public/assets/shiroko-icon.webp and b/public/assets/shiroko-icon.webp differ
diff --git a/public/assets/style.css b/public/assets/style.css
index cc559f9..cc4a023 100644
--- a/public/assets/style.css
+++ b/public/assets/style.css
@@ -5,10 +5,13 @@
--text: #111827;
--text-2: #6b7280;
--up: #16a34a;
+ --up-text: #15803d;
--up-bg: #f0fdf4;
--down: #dc2626;
+ --down-text: #b91c1c;
--down-bg: #fef2f2;
--pending: #d97706;
+ --pending-text: #92400e;
--pending-bg: #fffbeb;
--accent: #2563eb;
--uptime-ok: #16a34a;
@@ -28,10 +31,13 @@
--text: #e5e7eb;
--text-2: #9ca3af;
--up: #4ade80;
+ --up-text: #4ade80;
--up-bg: #10281c;
--down: #f87171;
+ --down-text: #f87171;
--down-bg: #2a1517;
--pending: #fbbf24;
+ --pending-text: #fbbf24;
--pending-bg: #2a2113;
--accent: #60a5fa;
--uptime-none: #2a2f3a;
@@ -101,15 +107,15 @@ body {
font-weight: 650;
white-space: nowrap;
}
-.pill.ok { background: var(--up-bg); color: var(--up); }
-.pill.bad { background: var(--down-bg); color: var(--down); }
-.pill.pending { background: var(--pending-bg); color: var(--pending); }
+.pill.ok { background: var(--up-bg); color: var(--up-text); }
+.pill.bad { background: var(--down-bg); color: var(--down-text); }
+.pill.pending { background: var(--pending-bg); color: var(--pending-text); }
.stat { text-align: center; min-width: 44px; }
.stat b { display: block; font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat label { font-size: 11px; color: var(--text-2); }
-.stat-up b { color: var(--up); }
-.stat-down b { color: var(--down); }
+.stat-up b { color: var(--up-text); }
+.stat-down b { color: var(--down-text); }
#refresh-btn {
display: inline-flex;
@@ -150,9 +156,6 @@ body {
.icon-btn:hover { color: var(--accent); border-color: var(--accent); }
.icon-btn.active { color: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); }
-/* 光环仅在白子主题下显示 */
-.logo .halo { display: none; }
-
/* ---------- 视图切换 ---------- */
.view-switch { display: flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.view-btn {
@@ -286,7 +289,7 @@ body {
overflow: hidden;
text-overflow: ellipsis;
}
-.card[data-status="down"] .card-detail { color: var(--down); }
+.card[data-status="down"] .card-detail { color: var(--down-text); }
.empty {
grid-column: 1 / -1;
@@ -322,8 +325,8 @@ body {
color: var(--text-2);
}
.footer .version { margin-left: auto; }
-.demo-chip { padding: 1px 8px; border-radius: 999px; background: var(--pending-bg); color: var(--pending); font-weight: 600; }
-.warn-chip { padding: 1px 8px; border-radius: 999px; background: var(--down-bg); color: var(--down); font-weight: 600; }
+.demo-chip { padding: 1px 8px; border-radius: 999px; background: var(--pending-bg); color: var(--pending-text); font-weight: 600; }
+.warn-chip { padding: 1px 8px; border-radius: 999px; background: var(--down-bg); color: var(--down-text); font-weight: 600; }
/* 窄屏(手机 / 竖屏平板):顶栏改为三行——标题 / 统计 / 控件,避免挤成一团 */
@media (max-width: 820px) {
@@ -374,9 +377,9 @@ body[data-view="table"] .grid { display: block; }
.t-state { white-space: nowrap; }
.t-state .dot { display: inline-block; margin-right: 6px; vertical-align: middle; }
-tr[data-status="up"] .t-state { color: var(--up); }
-tr[data-status="down"] .t-state { color: var(--down); }
-tr[data-status="pending"] .t-state { color: var(--pending); }
+tr[data-status="up"] .t-state { color: var(--up-text); }
+tr[data-status="down"] .t-state { color: var(--down-text); }
+tr[data-status="pending"] .t-state { color: var(--pending-text); }
.t-name { white-space: nowrap; }
.t-name .type-badge { margin-left: 8px; }
.t-target {
diff --git a/public/assets/theme-shiroko.css b/public/assets/theme-shiroko.css
index d118ba2..28dba27 100644
--- a/public/assets/theme-shiroko.css
+++ b/public/assets/theme-shiroko.css
@@ -14,22 +14,33 @@ body[data-theme="shiroko"] {
--surface: #ffffff;
--border: #d3e6f5;
--text: #17253d;
- --text-2: #5d7b9a;
- --up: #12b0a2;
+ --text-2: #55708c;
+ --up: #0da093;
+ --up-text: #0e7c72;
--up-bg: #e4faf6;
--down: #e0524e;
+ --down-text: #c0362f;
--down-bg: #fdecec;
- --pending: #d99413;
+ --pending: #c07d0d;
+ --pending-text: #8a5a00;
--pending-bg: #fff7e6;
- --accent: #1e9fd4;
- --uptime-ok: #12b0a2;
- --uptime-warn: #e5a52c;
+ --accent: #0e7490;
+ --uptime-ok: #0da093;
+ --uptime-warn: #c07d0d;
--uptime-bad: #e0524e;
--uptime-none: #dbe8f3;
--shadow: 0 1px 2px rgb(23 60 100 / 0.05), 0 10px 26px -20px rgb(23 60 100 / 0.35);
--sh-sky: #8fe0ff;
--sh-sky-deep: #2ea8dc;
--sh-navy: #1c2f4d;
+ /* 面板/表头/分组的底色与标题色随明暗模式切换(此前是硬编码浅色,暗色模式会「白顶栏 + 深页面」) */
+ --sh-panel-1: #ffffff;
+ --sh-panel-2: #f6fcff;
+ --sh-card-2: #fbfdff;
+ --sh-th-1: #f7fcff;
+ --sh-th-2: #eef7fd;
+ --sh-group: #eaf6fd;
+ --sh-title: #1c2f4d;
}
@media (prefers-color-scheme: dark) {
@@ -40,15 +51,25 @@ body[data-theme="shiroko"] {
--text: #e4eef7;
--text-2: #8ba6bf;
--up: #2fd4c2;
+ --up-text: #4fe0cf;
--up-bg: #0d2a2a;
--down: #f4736c;
+ --down-text: #f9a29c;
--down-bg: #2c1518;
--pending: #e8b344;
+ --pending-text: #f0c76a;
--pending-bg: #2a2113;
--accent: #55c2ef;
--uptime-ok: #2fd4c2;
--uptime-none: #1a2b3d;
--shadow: none;
+ --sh-panel-1: #14243a;
+ --sh-panel-2: #101d2c;
+ --sh-card-2: #0e1a27;
+ --sh-th-1: #16273c;
+ --sh-th-2: #101d2c;
+ --sh-group: #132437;
+ --sh-title: #e4eef7;
}
}
@@ -89,8 +110,8 @@ body[data-theme="shiroko"]::before {
height: 93px;
pointer-events: none;
z-index: 0;
- /* 半透明 + 两端渐隐,弱化「一块贴图」的突兀感 */
- opacity: 0.8;
+ /* 全局半透明 + 两端渐隐,弱化「一块贴图」的突兀感 */
+ opacity: var(--sh-deco-opacity, 0.78);
filter: saturate(0.92);
mask-image: linear-gradient(180deg, transparent 0, #000 16%, #000 84%, transparent 100%);
-webkit-mask-image: linear-gradient(180deg, transparent 0, #000 16%, #000 84%, transparent 100%);
@@ -99,7 +120,7 @@ body[data-theme="shiroko"]::before {
/* ---------- 顶栏:天蓝到白 + 底部围巾飘带 ---------- */
body[data-theme="shiroko"] .topbar {
- background: linear-gradient(180deg, #ffffff 0%, #f6fcff 100%);
+ background: linear-gradient(180deg, var(--sh-panel-1) 0%, var(--sh-panel-2) 100%);
border-bottom: 0;
box-shadow: var(--shadow);
overflow: hidden;
@@ -130,7 +151,7 @@ body[data-theme="shiroko"] .logo {
100% { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--sh-sky-deep), 0 0 12px rgb(46 168 220 / 55%); }
}
body[data-theme="shiroko"] .brand-text h1 {
- color: var(--sh-navy);
+ color: var(--sh-title);
letter-spacing: 0.04em;
}
@@ -178,7 +199,7 @@ body[data-theme="shiroko"] .card[data-status="up"] .dot { animation: halo-pulse
/* ---------- 卡片 / 表格 / 交互 ---------- */
body[data-theme="shiroko"] .card {
border-left-width: 3px;
- background: linear-gradient(180deg, #ffffff, #fbfdff);
+ background: linear-gradient(180deg, var(--sh-panel-1), var(--sh-card-2));
}
body[data-theme="shiroko"] .tab.active,
body[data-theme="shiroko"] .view-btn.active {
@@ -187,15 +208,15 @@ body[data-theme="shiroko"] .view-btn.active {
}
body[data-theme="shiroko"] #refresh-btn:hover { border-color: var(--accent); color: var(--accent); }
body[data-theme="shiroko"] .monitor-table th {
- background: linear-gradient(180deg, #f7fcff, #eef7fd);
- color: var(--sh-navy);
+ background: linear-gradient(180deg, var(--sh-th-1), var(--sh-th-2));
+ color: var(--sh-title);
border-bottom: 1px solid var(--border);
}
body[data-theme="shiroko"] .monitor-table tbody tr:hover { background: rgb(46 168 220 / 6%); }
body[data-theme="shiroko"] .group-row td {
- background: linear-gradient(90deg, #eaf6fd, #f7fcff);
+ background: linear-gradient(90deg, var(--sh-group), var(--sh-th-1));
border-left: 3px solid var(--sh-sky-deep);
- color: var(--sh-navy);
+ color: var(--sh-title);
}
body[data-theme="shiroko"] .table-wrap { box-shadow: var(--shadow); }
body[data-theme="shiroko"] .uptime-cell.lv-none {
@@ -208,20 +229,21 @@ body[data-theme="shiroko"] #refresh-btn .wheel { animation: wheel-spin 0.9s line
@keyframes wheel-spin { to { transform: rotate(360deg); } }
/* ---------- 简约风立绘(settings.artwork,默认指向内置形象) ---------- */
+/* --sh-deco-opacity 由面板按 settings.decoOpacity 注入,人物与单车共用同一个值 */
body[data-theme="shiroko"] .artwork {
position: fixed;
right: 10px;
bottom: 0;
width: auto;
- height: min(44vh, 380px);
+ height: min(40vh, 340px);
z-index: 0;
pointer-events: none;
user-select: none;
- /* 半透明 + 上下渐隐遮罩:让它像背景插画而不是贴上去的贴纸 */
- opacity: 0.92;
- filter: drop-shadow(0 6px 18px rgb(23 60 100 / 12%));
- mask-image: linear-gradient(180deg, transparent 0, #000 5%, #000 90%, rgb(0 0 0 / 40%) 100%);
- -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 5%, #000 90%, rgb(0 0 0 / 40%) 100%);
+ /* 全局半透明(整张图统一淡化)+ 上下渐隐,读起来是背景插画而不是贴上去的贴纸 */
+ opacity: var(--sh-deco-opacity, 0.78);
+ filter: drop-shadow(0 6px 18px rgb(23 60 100 / 10%));
+ mask-image: linear-gradient(180deg, transparent 0, #000 4%, #000 92%, rgb(0 0 0 / 45%) 100%);
+ -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 4%, #000 92%, rgb(0 0 0 / 45%) 100%);
}
/* 窄屏:立绘与固定装饰全部撤掉,避免压住内容或溢出(手机只保留配色与围巾飘带) */
@media (max-width: 820px) {
@@ -233,8 +255,7 @@ body[data-theme="shiroko"] .artwork {
@media (max-height: 520px) {
body[data-theme="shiroko"] .artwork { display: none; }
}
-/* 卡片视图行数多、卡片更靠右下,立绘再收一档,尽量让它落在卡片之外的角落 */
-body[data-theme="shiroko"][data-view="cards"] .artwork { height: min(34vh, 300px); }
+/* 注:立绘尺寸在卡片/表格两种视图下必须保持一致,不再按视图改高度 */
/* 内容层置于装饰之上 */
body[data-theme="shiroko"] .grid,
diff --git a/public/index.html b/public/index.html
index 352a58c..b210701 100644
--- a/public/index.html
+++ b/public/index.html
@@ -56,7 +56,7 @@
-
+
diff --git a/public/monitors.json b/public/monitors.json
index 3bd20bd..30a2fdf 100644
--- a/public/monitors.json
+++ b/public/monitors.json
@@ -5,6 +5,7 @@
"view": "table",
"theme": "shiroko",
"artwork": "assets/shiroko-figure.webp",
+ "decoOpacity": 0.78,
"dataFilter": "uptimerobot",
"refreshInterval": 300,
"dataSource": "auto",
diff --git a/public/status.json b/public/status.json
index 3d562c9..c088903 100644
--- a/public/status.json
+++ b/public/status.json
@@ -1,8 +1,8 @@
{
"prober": "shiroko-monitor-node/0.7.0",
"demo": false,
- "generatedAt": "2026-10-13T13:06:27.328Z",
- "dataUpdatedAt": "2026-10-13T13:05:50.691Z",
+ "generatedAt": "2026-10-13T13:55:30.243Z",
+ "dataUpdatedAt": "2026-10-13T13:54:57.645Z",
"updateIntervalSec": 300,
"dataFilter": "uptimerobot",
"stats": {
@@ -10,11 +10,11 @@
"up": 5,
"down": 1,
"pending": 0,
- "avgLatency": 991,
+ "avgLatency": 1110,
"avgUptime90": 83.3,
"memory": {
- "rssMB": 77.5,
- "heapUsedMB": 10.7,
+ "rssMB": 78.9,
+ "heapUsedMB": 10.2,
"heapTotalMB": 16
}
},
@@ -23,200 +23,200 @@
"enabled": true,
"ok": true,
"error": null,
- "lastSyncAt": "2026-10-13T13:03:02.502Z",
+ "lastSyncAt": "2026-10-13T13:52:32.984Z",
"count": 6,
"syncing": false,
- "lastSyncMs": 1791896580947
+ "lastSyncMs": 1791899551553
}
},
"monitors": {
"ur-792059439": {
"status": "up",
- "latency": 277,
+ "latency": 247,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-13T12:58:00.000Z",
+ "lastCheck": "2026-10-13T13:48:00.000Z",
"history": [
{
- "t": "2026-10-12T13:50:00.000Z",
+ "t": "2026-10-12T15:00:00.000Z",
"ok": true,
- "ms": 732
+ "ms": 312
},
{
- "t": "2026-10-12T14:30:00.000Z",
+ "t": "2026-10-12T15:40:00.000Z",
"ok": true,
- "ms": 663
+ "ms": 336
},
{
- "t": "2026-10-12T15:10:00.000Z",
+ "t": "2026-10-12T16:20:00.000Z",
"ok": true,
- "ms": 433
+ "ms": 275
},
{
- "t": "2026-10-12T15:50:00.000Z",
+ "t": "2026-10-12T17:00:00.000Z",
"ok": true,
- "ms": 355
+ "ms": 255
},
{
- "t": "2026-10-12T16:30:00.000Z",
+ "t": "2026-10-12T17:40:00.000Z",
"ok": true,
- "ms": 293
+ "ms": 304
},
{
- "t": "2026-10-12T17:10:00.000Z",
+ "t": "2026-10-12T18:20:00.000Z",
"ok": true,
- "ms": 220
+ "ms": 323
},
{
- "t": "2026-10-12T17:55:00.000Z",
+ "t": "2026-10-12T19:00:00.000Z",
"ok": true,
- "ms": 321
+ "ms": 328
},
{
- "t": "2026-10-12T18:35:00.000Z",
+ "t": "2026-10-12T19:40:00.000Z",
"ok": true,
- "ms": 295
+ "ms": 304
},
{
- "t": "2026-10-12T19:15:00.000Z",
+ "t": "2026-10-12T20:20:00.000Z",
+ "ok": true,
+ "ms": 265
+ },
+ {
+ "t": "2026-10-12T21:00:00.000Z",
+ "ok": true,
+ "ms": 294
+ },
+ {
+ "t": "2026-10-12T21:40:00.000Z",
+ "ok": true,
+ "ms": 379
+ },
+ {
+ "t": "2026-10-12T22:20:00.000Z",
+ "ok": true,
+ "ms": 176
+ },
+ {
+ "t": "2026-10-12T23:05:00.000Z",
+ "ok": true,
+ "ms": 286
+ },
+ {
+ "t": "2026-10-12T23:45:00.000Z",
+ "ok": true,
+ "ms": 219
+ },
+ {
+ "t": "2026-10-13T00:25:00.000Z",
+ "ok": true,
+ "ms": 207
+ },
+ {
+ "t": "2026-10-13T01:05:00.000Z",
+ "ok": true,
+ "ms": 162
+ },
+ {
+ "t": "2026-10-13T01:45:00.000Z",
+ "ok": true,
+ "ms": 160
+ },
+ {
+ "t": "2026-10-13T02:25:00.000Z",
+ "ok": true,
+ "ms": 199
+ },
+ {
+ "t": "2026-10-13T03:05:00.000Z",
+ "ok": true,
+ "ms": 188
+ },
+ {
+ "t": "2026-10-13T03:45:00.000Z",
+ "ok": true,
+ "ms": 130
+ },
+ {
+ "t": "2026-10-13T04:25:00.000Z",
+ "ok": true,
+ "ms": 494
+ },
+ {
+ "t": "2026-10-13T05:05:00.000Z",
"ok": true,
"ms": 310
},
{
- "t": "2026-10-12T19:55:00.000Z",
+ "t": "2026-10-13T05:45:00.000Z",
"ok": true,
- "ms": 256
+ "ms": 354
},
{
- "t": "2026-10-12T20:35:00.000Z",
+ "t": "2026-10-13T06:25:00.000Z",
"ok": true,
- "ms": 185
+ "ms": 275
},
{
- "t": "2026-10-12T21:15:00.000Z",
+ "t": "2026-10-13T07:05:00.000Z",
"ok": true,
- "ms": 293
+ "ms": 231
},
{
- "t": "2026-10-12T21:55:00.000Z",
+ "t": "2026-10-13T07:45:00.000Z",
"ok": true,
- "ms": 270
+ "ms": 237
},
{
- "t": "2026-10-12T22:35:00.000Z",
+ "t": "2026-10-13T08:25:00.000Z",
"ok": true,
- "ms": 181
+ "ms": 467
},
{
- "t": "2026-10-12T23:15:00.000Z",
+ "t": "2026-10-13T09:05:00.000Z",
"ok": true,
- "ms": 225
+ "ms": 409
},
{
- "t": "2026-10-12T23:55:00.000Z",
+ "t": "2026-10-13T09:45:00.000Z",
"ok": true,
- "ms": 387
+ "ms": 424
},
{
- "t": "2026-10-13T00:35:00.000Z",
+ "t": "2026-10-13T10:25:00.000Z",
"ok": true,
- "ms": 288
+ "ms": 171
},
{
- "t": "2026-10-13T01:15:00.000Z",
+ "t": "2026-10-13T11:05:00.000Z",
"ok": true,
- "ms": 167
+ "ms": 377
},
{
- "t": "2026-10-13T01:55:00.000Z",
+ "t": "2026-10-13T11:45:00.000Z",
"ok": true,
- "ms": 157
+ "ms": 304
},
{
- "t": "2026-10-13T02:35:00.000Z",
+ "t": "2026-10-13T12:25:00.000Z",
"ok": true,
- "ms": 228
+ "ms": 426
},
{
- "t": "2026-10-13T03:15:00.000Z",
+ "t": "2026-10-13T13:08:00.000Z",
"ok": true,
- "ms": 225
+ "ms": 250
},
{
- "t": "2026-10-13T03:55:00.000Z",
+ "t": "2026-10-13T13:48:00.000Z",
"ok": true,
- "ms": 99
- },
- {
- "t": "2026-10-13T04:40:00.000Z",
- "ok": true,
- "ms": 471
- },
- {
- "t": "2026-10-13T05:20:00.000Z",
- "ok": true,
- "ms": 305
- },
- {
- "t": "2026-10-13T06:00:00.000Z",
- "ok": true,
- "ms": 314
- },
- {
- "t": "2026-10-13T06:40:00.000Z",
- "ok": true,
- "ms": 245
- },
- {
- "t": "2026-10-13T07:20:00.000Z",
- "ok": true,
- "ms": 295
- },
- {
- "t": "2026-10-13T08:00:00.000Z",
- "ok": true,
- "ms": 262
- },
- {
- "t": "2026-10-13T08:40:00.000Z",
- "ok": true,
- "ms": 440
- },
- {
- "t": "2026-10-13T09:20:00.000Z",
- "ok": true,
- "ms": 302
- },
- {
- "t": "2026-10-13T10:00:00.000Z",
- "ok": true,
- "ms": 437
- },
- {
- "t": "2026-10-13T10:40:00.000Z",
- "ok": true,
- "ms": 246
- },
- {
- "t": "2026-10-13T11:20:00.000Z",
- "ok": true,
- "ms": 395
- },
- {
- "t": "2026-10-13T12:22:00.000Z",
- "ok": true,
- "ms": 302
- },
- {
- "t": "2026-10-13T12:58:00.000Z",
- "ok": true,
- "ms": 277
+ "ms": 247
}
],
"daily": {
"2026-10-13": {
- "total": 23,
- "up": 23
+ "total": 35,
+ "up": 35
}
},
"meta": {
@@ -243,16 +243,11 @@
},
"ur-795497022": {
"status": "up",
- "latency": 749,
+ "latency": 1131,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-13T13:02:00.000Z",
+ "lastCheck": "2026-10-13T13:50:00.000Z",
"history": [
- {
- "t": "2026-10-12T13:55:00.000Z",
- "ok": true,
- "ms": 1017
- },
{
"t": "2026-10-12T14:35:00.000Z",
"ok": true,
@@ -414,20 +409,25 @@
"ms": 740
},
{
- "t": "2026-10-13T12:25:00.000Z",
+ "t": "2026-10-13T12:40:00.000Z",
"ok": true,
- "ms": 819
+ "ms": 771
},
{
- "t": "2026-10-13T13:02:00.000Z",
+ "t": "2026-10-13T13:13:00.000Z",
"ok": true,
- "ms": 749
+ "ms": 772
+ },
+ {
+ "t": "2026-10-13T13:50:00.000Z",
+ "ok": true,
+ "ms": 1131
}
],
"daily": {
"2026-10-13": {
- "total": 23,
- "up": 23
+ "total": 35,
+ "up": 35
}
},
"meta": {
@@ -454,191 +454,191 @@
},
"ur-795556828": {
"status": "up",
- "latency": 414,
+ "latency": 224,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-13T12:58:00.000Z",
+ "lastCheck": "2026-10-13T13:49:00.000Z",
"history": [
{
- "t": "2026-10-12T13:50:00.000Z",
+ "t": "2026-10-12T14:55:00.000Z",
"ok": true,
- "ms": 276
+ "ms": 225
},
{
- "t": "2026-10-12T14:30:00.000Z",
+ "t": "2026-10-12T15:35:00.000Z",
"ok": true,
- "ms": 288
+ "ms": 275
},
{
- "t": "2026-10-12T15:10:00.000Z",
+ "t": "2026-10-12T16:15:00.000Z",
"ok": true,
- "ms": 240
+ "ms": 450
},
{
- "t": "2026-10-12T15:50:00.000Z",
+ "t": "2026-10-12T16:55:00.000Z",
"ok": true,
- "ms": 348
+ "ms": 281
},
{
- "t": "2026-10-12T16:30:00.000Z",
+ "t": "2026-10-12T17:40:00.000Z",
"ok": true,
- "ms": 378
+ "ms": 320
},
{
- "t": "2026-10-12T17:10:00.000Z",
+ "t": "2026-10-12T18:20:00.000Z",
"ok": true,
- "ms": 295
+ "ms": 287
},
{
- "t": "2026-10-12T17:50:00.000Z",
+ "t": "2026-10-12T19:00:00.000Z",
"ok": true,
- "ms": 317
+ "ms": 283
},
{
- "t": "2026-10-12T18:30:00.000Z",
+ "t": "2026-10-12T19:40:00.000Z",
"ok": true,
- "ms": 319
+ "ms": 464
},
{
- "t": "2026-10-12T19:10:00.000Z",
+ "t": "2026-10-12T20:20:00.000Z",
"ok": true,
- "ms": 252
+ "ms": 187
},
{
- "t": "2026-10-12T19:50:00.000Z",
+ "t": "2026-10-12T21:00:00.000Z",
"ok": true,
- "ms": 551
+ "ms": 233
},
{
- "t": "2026-10-12T20:30:00.000Z",
- "ok": true,
- "ms": 235
- },
- {
- "t": "2026-10-12T21:10:00.000Z",
- "ok": true,
- "ms": 294
- },
- {
- "t": "2026-10-12T21:50:00.000Z",
- "ok": true,
- "ms": 186
- },
- {
- "t": "2026-10-12T22:30:00.000Z",
- "ok": true,
- "ms": 183
- },
- {
- "t": "2026-10-12T23:10:00.000Z",
- "ok": true,
- "ms": 244
- },
- {
- "t": "2026-10-12T23:50:00.000Z",
- "ok": true,
- "ms": 206
- },
- {
- "t": "2026-10-13T00:30:00.000Z",
- "ok": true,
- "ms": 197
- },
- {
- "t": "2026-10-13T01:10:00.000Z",
- "ok": true,
- "ms": 155
- },
- {
- "t": "2026-10-13T01:50:00.000Z",
- "ok": true,
- "ms": 119
- },
- {
- "t": "2026-10-13T02:30:00.000Z",
+ "t": "2026-10-12T21:40:00.000Z",
"ok": true,
"ms": 213
},
{
- "t": "2026-10-13T03:10:00.000Z",
+ "t": "2026-10-12T22:20:00.000Z",
"ok": true,
- "ms": 151
+ "ms": 235
},
{
- "t": "2026-10-13T03:50:00.000Z",
+ "t": "2026-10-12T23:05:00.000Z",
"ok": true,
- "ms": 231
+ "ms": 241
},
{
- "t": "2026-10-13T04:35:00.000Z",
+ "t": "2026-10-12T23:45:00.000Z",
"ok": true,
- "ms": 194
+ "ms": 230
},
{
- "t": "2026-10-13T05:15:00.000Z",
+ "t": "2026-10-13T00:25:00.000Z",
"ok": true,
- "ms": 211
+ "ms": 244
},
{
- "t": "2026-10-13T05:55:00.000Z",
+ "t": "2026-10-13T01:05:00.000Z",
"ok": true,
- "ms": 167
+ "ms": 133
},
{
- "t": "2026-10-13T06:35:00.000Z",
+ "t": "2026-10-13T01:45:00.000Z",
"ok": true,
- "ms": 240
+ "ms": 127
},
{
- "t": "2026-10-13T07:15:00.000Z",
+ "t": "2026-10-13T02:25:00.000Z",
"ok": true,
- "ms": 254
+ "ms": 182
},
{
- "t": "2026-10-13T07:55:00.000Z",
+ "t": "2026-10-13T03:05:00.000Z",
"ok": true,
- "ms": 503
+ "ms": 413
},
{
- "t": "2026-10-13T08:35:00.000Z",
+ "t": "2026-10-13T03:45:00.000Z",
"ok": true,
- "ms": 224
+ "ms": 229
},
{
- "t": "2026-10-13T09:15:00.000Z",
+ "t": "2026-10-13T04:25:00.000Z",
+ "ok": true,
+ "ms": 255
+ },
+ {
+ "t": "2026-10-13T05:05:00.000Z",
+ "ok": true,
+ "ms": 150
+ },
+ {
+ "t": "2026-10-13T05:45:00.000Z",
+ "ok": true,
+ "ms": 201
+ },
+ {
+ "t": "2026-10-13T06:25:00.000Z",
+ "ok": true,
+ "ms": 232
+ },
+ {
+ "t": "2026-10-13T07:05:00.000Z",
"ok": true,
"ms": 247
},
{
- "t": "2026-10-13T10:00:00.000Z",
+ "t": "2026-10-13T07:45:00.000Z",
"ok": true,
- "ms": 455
+ "ms": 559
},
{
- "t": "2026-10-13T10:40:00.000Z",
+ "t": "2026-10-13T08:25:00.000Z",
"ok": true,
- "ms": 384
+ "ms": 219
},
{
- "t": "2026-10-13T11:20:00.000Z",
+ "t": "2026-10-13T09:05:00.000Z",
"ok": true,
- "ms": 334
+ "ms": 230
},
{
- "t": "2026-10-13T12:22:00.000Z",
+ "t": "2026-10-13T09:45:00.000Z",
"ok": true,
- "ms": 393
+ "ms": 432
},
{
- "t": "2026-10-13T12:58:00.000Z",
+ "t": "2026-10-13T10:25:00.000Z",
"ok": true,
- "ms": 414
+ "ms": 341
+ },
+ {
+ "t": "2026-10-13T11:05:00.000Z",
+ "ok": true,
+ "ms": 233
+ },
+ {
+ "t": "2026-10-13T11:45:00.000Z",
+ "ok": true,
+ "ms": 396
+ },
+ {
+ "t": "2026-10-13T12:25:00.000Z",
+ "ok": true,
+ "ms": 308
+ },
+ {
+ "t": "2026-10-13T13:13:00.000Z",
+ "ok": true,
+ "ms": 347
+ },
+ {
+ "t": "2026-10-13T13:49:00.000Z",
+ "ok": true,
+ "ms": 224
}
],
"daily": {
"2026-10-13": {
- "total": 23,
- "up": 23
+ "total": 35,
+ "up": 35
}
},
"meta": {
@@ -665,16 +665,11 @@
},
"ur-792059414": {
"status": "up",
- "latency": 3103,
+ "latency": 3462,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-13T12:59:00.000Z",
+ "lastCheck": "2026-10-13T13:50:00.000Z",
"history": [
- {
- "t": "2026-10-12T14:10:00.000Z",
- "ok": true,
- "ms": 2478
- },
{
"t": "2026-10-12T14:55:00.000Z",
"ok": true,
@@ -801,40 +796,50 @@
"ms": 1861
},
{
- "t": "2026-10-13T09:10:00.000Z",
+ "t": "2026-10-13T09:05:00.000Z",
"ok": true,
- "ms": 1991
+ "ms": 2001
},
{
- "t": "2026-10-13T09:55:00.000Z",
+ "t": "2026-10-13T09:45:00.000Z",
"ok": true,
- "ms": 1597
+ "ms": 1652
},
{
- "t": "2026-10-13T10:40:00.000Z",
+ "t": "2026-10-13T10:25:00.000Z",
"ok": true,
- "ms": 2816
+ "ms": 2264
},
{
- "t": "2026-10-13T11:20:00.000Z",
+ "t": "2026-10-13T11:05:00.000Z",
"ok": true,
- "ms": 1960
+ "ms": 2105
},
{
- "t": "2026-10-13T12:19:00.000Z",
+ "t": "2026-10-13T11:45:00.000Z",
"ok": true,
- "ms": 2258
+ "ms": 1562
},
{
- "t": "2026-10-13T12:59:00.000Z",
+ "t": "2026-10-13T12:25:00.000Z",
"ok": true,
- "ms": 3103
+ "ms": 2773
+ },
+ {
+ "t": "2026-10-13T13:10:00.000Z",
+ "ok": true,
+ "ms": 2536
+ },
+ {
+ "t": "2026-10-13T13:50:00.000Z",
+ "ok": true,
+ "ms": 3462
}
],
"daily": {
"2026-10-13": {
- "total": 23,
- "up": 23
+ "total": 35,
+ "up": 35
}
},
"meta": {
@@ -861,21 +866,11 @@
},
"ur-798788113": {
"status": "up",
- "latency": 410,
+ "latency": 488,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-13T12:59:00.000Z",
+ "lastCheck": "2026-10-13T13:50:00.000Z",
"history": [
- {
- "t": "2026-10-12T13:45:00.000Z",
- "ok": true,
- "ms": 361
- },
- {
- "t": "2026-10-12T14:25:00.000Z",
- "ok": true,
- "ms": 346
- },
{
"t": "2026-10-12T15:05:00.000Z",
"ok": true,
@@ -1032,25 +1027,30 @@
"ms": 381
},
{
- "t": "2026-10-13T11:43:00.000Z",
+ "t": "2026-10-13T11:45:00.000Z",
"ok": true,
- "ms": 1257
+ "ms": 1028
},
{
- "t": "2026-10-13T12:23:00.000Z",
+ "t": "2026-10-13T12:25:00.000Z",
"ok": true,
- "ms": 471
+ "ms": 601
},
{
- "t": "2026-10-13T12:59:00.000Z",
+ "t": "2026-10-13T13:14:00.000Z",
"ok": true,
- "ms": 410
+ "ms": 428
+ },
+ {
+ "t": "2026-10-13T13:50:00.000Z",
+ "ok": true,
+ "ms": 488
}
],
"daily": {
"2026-10-13": {
- "total": 23,
- "up": 23
+ "total": 35,
+ "up": 35
}
},
"meta": {
@@ -1090,7 +1090,7 @@
],
"daily": {
"2026-10-13": {
- "total": 23,
+ "total": 35,
"up": 0
}
},
diff --git a/scripts/prepare-artwork.py b/scripts/prepare-artwork.py
new file mode 100644
index 0000000..df04814
--- /dev/null
+++ b/scripts/prepare-artwork.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+"""从 assets-src/ 的原始插画生成 public/assets/ 下的透明素材。
+
+为什么要这么处理:
+ 图像生成返回的是**白底**图,需要抠成透明。抠白底分两步,缺一不可——
+ 1) 边界洪泛:把与图片边界连通的白色变透明(背景主体)
+ 2) 封闭区域补抠:被图形围起来的白色区域与边界不连通,第 1 步碰不到它们。
+ 光环环内、单车车架三角、轮圈内扇形都会因此留成白块(v0.8.3 之前就踩过)
+ 注意:不能简单「全局抠白」,那会把角色的脸/袜子/衣服白一起抠掉。
+ 本脚本按连通域面积处理,并跳过角色躯干区域,保留服装白。
+
+用法(PyPI 的 Pillow):
+ python scripts/prepare-artwork.py
+"""
+from collections import deque
+from pathlib import Path
+
+from PIL import Image, ImageDraw
+
+ROOT = Path(__file__).resolve().parent.parent
+SRC = ROOT / "assets-src"
+DST = ROOT / "public" / "assets"
+SENTINEL = (255, 0, 255)
+WHITE_TOLERANCE = 18 # 与纯白的最大通道差
+
+
+def is_white(px):
+ return px[3] > 128 and px[0] > 255 - WHITE_TOLERANCE and px[1] > 255 - WHITE_TOLERANCE and px[2] > 255 - WHITE_TOLERANCE
+
+
+def connected_regions(im, w, h):
+ """列出所有「近白且连通」的区域:[(面积, [(x, y), ...]), ...]"""
+ px = im.load()
+ seen = [[False] * w for _ in range(h)]
+ regions = []
+ for y in range(h):
+ for x in range(w):
+ if seen[y][x] or not is_white(px[x, y]):
+ continue
+ queue = deque([(x, y)])
+ seen[y][x] = True
+ cells = []
+ while queue:
+ cx, cy = queue.popleft()
+ cells.append((cx, cy))
+ for nx, ny in ((cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)):
+ if 0 <= nx < w and 0 <= ny < h and not seen[ny][nx] and is_white(px[nx, ny]):
+ seen[ny][nx] = True
+ queue.append((nx, ny))
+ regions.append((len(cells), cells))
+ return regions
+
+
+def cut_out(src: Path, dst: Path, target_height: int, *, min_area: int, square: bool = False,
+ skip_below: float | None = None, quality: int = 92):
+ """抠白底 -> 裁边 -> 按高度缩放 -> 存 webp。
+ square: 裁边后补成正方形(头像/图标用,避免圆形裁切与 favicon 变形)
+ skip_below: 只清理位于该纵向比例(0~1)以上的封闭区域;None 表示全部清理
+ """
+ im = Image.open(src).convert("RGBA")
+ width, height = im.size
+
+ # 第 1 步:边界洪泛(连通到边界的背景)
+ rgb = im.convert("RGB")
+ for x, y in ((0, 0), (width - 1, 0), (0, height - 1), (width - 1, height - 1),
+ (width // 2, 0), (0, height // 2), (width - 1, height // 2), (width // 2, height - 1)):
+ if rgb.getpixel((x, y)) != SENTINEL:
+ ImageDraw.floodfill(rgb, (x, y), SENTINEL, thresh=WHITE_TOLERANCE + 24)
+ sentinel_px = rgb.load()
+ px = im.load()
+ for y in range(height):
+ for x in range(width):
+ if sentinel_px[x, y] == SENTINEL:
+ px[x, y] = (0, 0, 0, 0)
+
+ # 第 2 步:封闭区域补抠(按面积阈值 + 可选纵向范围)
+ removed = 0
+ for area, cells in connected_regions(im, width, height):
+ if area < min_area:
+ continue
+ if skip_below is not None and min(cy for _, cy in cells) > height * skip_below:
+ continue # 疑似服装白,保留
+ for x, y in cells:
+ px[x, y] = (0, 0, 0, 0)
+ removed += area
+
+ box = im.getbbox()
+ if box:
+ im = im.crop(box)
+ if square:
+ side = max(im.size)
+ canvas = Image.new("RGBA", (side, side), (0, 0, 0, 0))
+ canvas.paste(im, ((side - im.width) // 2, (side - im.height) // 2))
+ im = canvas
+ scale = target_height / im.height
+ im = im.resize((round(im.width * scale), target_height), Image.LANCZOS)
+ im.save(dst, "WEBP", quality=quality, method=6)
+ print(f"{dst.name}: {im.size} 补抠封闭白区 {removed}px,体积 {dst.stat().st_size / 1024:.1f}KB")
+ return im
+
+
+def main():
+ # 立绘:560 高(与 CSS 里 min(40vh, 340px) 的显示尺寸对应,留 1.6 倍余量给高 DPI)
+ figure = cut_out(SRC / "figure-original.png", DST / "shiroko-figure.webp", 560, min_area=150)
+ # 图标:补成正方形再缩到 180,供圆形头像与 favicon 使用
+ icon = cut_out(SRC / "icon-original.png", DST / "shiroko-icon.webp", 180, min_area=15, square=True)
+ icon.resize((64, 64), Image.LANCZOS).save(DST / "favicon-shiroko.png", "PNG", optimize=True)
+ print(f"favicon-shiroko.png: 64x64,体积 {(DST / 'favicon-shiroko.png').stat().st_size / 1024:.1f}KB")
+ # 单车:200 高,通体无服装白,全部清理
+ cut_out(SRC / "bike-original.png", DST / "shiroko-bike.webp", 200, min_area=20)
+ print("完成。视觉上请确认:光环环内、车架三角、轮圈内扇形均为透明,且角色的脸/袜子/衣服完好。")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/verify-views.cjs b/scripts/verify-views.cjs
index dea7145..1c19ab8 100644
--- a/scripts/verify-views.cjs
+++ b/scripts/verify-views.cjs
@@ -71,7 +71,13 @@ async function main() {
const hits = [...document.querySelectorAll('table.monitor-table tbody tr, .card')]
.map((el) => ({ r: el.getBoundingClientRect(), el }))
.filter(({ r }) => r.right > a.left + 8 && r.bottom > a.top && r.top < a.bottom)
- .map(({ el }) => opaque(getComputedStyle(el).backgroundColor) || opaque(getComputedStyle(el.closest('.table-wrap, .grid') ?? el).backgroundColor));
+ .map(({ el }) => {
+ // 不透明判定要同时看背景色与背景图(卡片用的是渐变背景,backgroundColor 仍是透明)
+ const cs = getComputedStyle(el);
+ if (cs.backgroundImage !== 'none') return true;
+ const chain = [cs, getComputedStyle(el.closest('.table-wrap, .grid') ?? el)];
+ return chain.some((s) => s.backgroundImage !== 'none' || opaque(s.backgroundColor));
+ });
return { count: hits.length, allOpaque: hits.every(Boolean) };
});
if (artOverlap) check(`${view} 视图立绘不影响阅读(不重叠或位于面板之下)`, artOverlap.count === 0 || artOverlap.allOpaque, `重叠 ${artOverlap.count} 个,均在面板之下=${artOverlap.allOpaque}`);
@@ -175,6 +181,69 @@ async function main() {
await mp.screenshot({ path: path.join(__dirname, '..', 'docs', 'screenshot-mobile.png'), fullPage: true });
await mCtx.close();
+ // ---------- I. 无障碍:文字与图形对比度(WCAG AA) ----------
+ console.log('\n[I] 对比度(WCAG AA:正文 ≥4.5,图形 ≥3.0)');
+ // 对比度需在表格视图下采样(在线/离线状态文字在表格行里)
+ await page.goto(`${BASE}/?source=api&view=table`, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('table.monitor-table tbody tr[data-status]', { timeout: 20000 });
+ await page.waitForTimeout(1000);
+ const contrast = await page.evaluate(() => {
+ // 颜色可能是 rgb()/rgba() 或 color(srgb r g b / a)(color-mix 的结果),统一解析后逐层合成,
+ // 否则半透明底色会被当成不透明、0~1 分量会被当成 0~255,算出的比值是错的
+ const parseColor = (c) => {
+ const nums = (c.match(/[\d.]+/g) ?? []).map(Number);
+ const srgb = c.startsWith('color(srgb');
+ const [r, g, b] = srgb ? nums.slice(0, 3).map((v) => v * 255) : nums.slice(0, 3);
+ const a = srgb ? (nums.length > 3 ? nums[3] : 1) : (nums.length > 3 ? nums[3] : 1);
+ return { r, g, b, a };
+ };
+ const effectiveBg = (el) => {
+ const chain = [];
+ for (let n = el; n; n = n.parentElement) chain.push(n);
+ let bg = { r: 255, g: 255, b: 255 };
+ for (const n of chain.reverse()) {
+ const c = parseColor(getComputedStyle(n).backgroundColor);
+ if (!c.a) continue;
+ bg = { r: bg.r * (1 - c.a) + c.r * c.a, g: bg.g * (1 - c.a) + c.g * c.a, b: bg.b * (1 - c.a) + c.b * c.a };
+ }
+ return `rgb(${Math.round(bg.r)}, ${Math.round(bg.g)}, ${Math.round(bg.b)})`;
+ };
+ const luminance = ([r, g, b]) => {
+ const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
+ return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
+ };
+ const ratio = (fg, bg) => {
+ const a = luminance([parseColor(fg).r, parseColor(fg).g, parseColor(fg).b]);
+ const b = luminance([parseColor(bg).r, parseColor(bg).g, parseColor(bg).b]);
+ const [hi, lo] = a > b ? [a, b] : [b, a];
+ return (hi + 0.05) / (lo + 0.05);
+ };
+ const sample = (sel, label) => {
+ const el = document.querySelector(sel);
+ if (!el) return null;
+ return { label, text: el.textContent.trim().slice(0, 8), r: +ratio(getComputedStyle(el).color, effectiveBg(el)).toFixed(2) };
+ };
+ const out = [
+ sample('tr[data-status="up"] .t-state', '在线状态文字'),
+ sample('tr[data-status="down"] .t-state', '离线状态文字'),
+ sample('tr[data-status="pending"] .t-state', '等待状态文字'),
+ sample('#stat-up', '在线统计数字'),
+ sample('#stat-down', '离线统计数字'),
+ sample('#overall-pill', '顶部状态胶囊'),
+ sample('.t-target', '目标地址'),
+ sample('.footer-item', '页脚文字'),
+ ].filter(Boolean);
+ // 图形:状态点边框色 vs 其所在卡片底色(图形标准 3:1)
+ const dot = document.querySelector('.dot');
+ if (dot) out.push({ label: '状态点图形', r: +ratio(getComputedStyle(dot).borderTopColor, effectiveBg(dot)).toFixed(2), graphic: true });
+ return out;
+ });
+ for (const c of contrast) {
+ if (c.r == null) continue;
+ const need = c.graphic ? 3 : 4.5;
+ check(`${c.label}对比度 ≥${need}`, c.r >= need, `${c.r}:1`);
+ }
+
// 429 是刷新限流保护在起作用(快速连续导航各触发一次强制探测),面板已优雅降级为只读数据;
// 这里只把 429 以外的错误视为失败
const hardErrors = errors.filter((e) => !/429/.test(e));