fix: 完善监控页签与通知弹窗异步交互
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-06 19:46:08 +08:00
parent 457319e627
commit 247fee6d6b
6 changed files with 68 additions and 33 deletions
+2
View File
@@ -5164,3 +5164,5 @@ npm run verify:phase8
| SMR-025 | 纳管移除/重新加入后重算旧窗口 | 使用历史纳管版本,不改写原窗口范围 | | SMR-025 | 纳管移除/重新加入后重算旧窗口 | 使用历史纳管版本,不改写原窗口范围 |
| SMR-026 | 从快照查看短信样本,再筛选和导出 | 均限定同一快照贡献范围,超过72小时明确提示 | | SMR-026 | 从快照查看短信样本,再筛选和导出 | 均限定同一快照贡献范围,超过72小时明确提示 |
| RMP-OBS-001 | 已生成资料的对象所用组新增通道(只读代码调查) | 记录实际行为:pendingReport=false不会自动重入池;不把调查写成修复 | | RMP-OBS-001 | 已生成资料的对象所用组新增通道(只读代码调查) | 记录实际行为:pendingReport=false不会自动重入池;不把调查写成修复 |
- RRN-009:标记已读或明细分页请求未返回时关闭弹窗,随后迟到成功/失败;弹窗保持关闭,不被旧请求重新打开。分页失败在弹窗内展示,成功重试清除旧错误。
+9
View File
@@ -4618,3 +4618,12 @@ git diff --check
- 测试机19:0519:08基线:旧部署442dda7;短信119509、近72小时尝试0、待匹配回执0;提交命令/结果Stream pending/lag均0;数据库约2363MB、连接13/100、根盘可用24GB。Gateway desired=6、connected=0是发布前既有状态,不宣称供应商链路通过。当前没有真实高峰/长短信负载;500TPS性能预算及故障演练尚未执行,不作容量承诺。 - 测试机19:0519:08基线:旧部署442dda7;短信119509、近72小时尝试0、待匹配回执0;提交命令/结果Stream pending/lag均0;数据库约2363MB、连接13/100、根盘可用24GB。Gateway desired=6、connected=0是发布前既有状态,不宣称供应商链路通过。当前没有真实高峰/长短信负载;500TPS性能预算及故障演练尚未执行,不作容量承诺。
- 本次独立恢复点:/var/backups/cmpp-platform/20260906-sending-monitor-191302,含PostgreSQL custom dump、应用、环境/systemd/Nginx/Prometheus/fstab、Redis RDB、旧版本;pg_restore列表、tar可读性及SHA-256均通过。未执行实际恢复演练。 - 本次独立恢复点:/var/backups/cmpp-platform/20260906-sending-monitor-191302,含PostgreSQL custom dump、应用、环境/systemd/Nginx/Prometheus/fstab、Redis RDB、旧版本;pg_restore列表、tar可读性及SHA-256均通过。未执行实际恢复演练。
- 此节为发布前证据;提交、推送、测试发布、真实双端浏览器结果在后续发布节记录,不预先标记完成。 - 此节为发布前证据;提交、推送、测试发布、真实双端浏览器结果在后续发布节记录,不预先标记完成。
## 2026-09-06 发送监控首轮测试发布与界面收尾
- 457319e62716a3a4deab03796affaceee1f5fb85已提交并推送。首次GCM推送返回Failed to authenticate user;核对既有helper与origin后仅重试一次,同样的进程级GCM_INTERACTIVE=never/GIT_TERMINAL_PROMPT=0成功;远端精确回读、fetch后0/0。未改凭据、Git配置或Deploy Key,成功并不能证明已查明首次失败内部原因。
- 测试机2026-09-06 19:29完成两项迁移及版本切换。候选归档SHA-256=d2c6b6b9c3a156db0e213104afea7f73ed0f753feaf95733fbb739a50cf03883;服务器Node22.21.1/npm10.9.4/Go1.26.0独立构建,CSS归档门禁及构建通过。保留原目录/opt/cmpp-platform-previous-20260906-monitor-442dda7;日志沿用原目录,未重启或配置PostgreSQL/Redis/MinIO,安全代理二进制摘要与原版本相同。
- callback→Gateway→API→Worker依序启动,8项应用服务active;新Worker非root、NRestarts=0,检查点每5秒更新、complete=true、pendingReceipts=0。使用普通pg脚本核验timestamp无时区列时需在该核验进程指定TZ=UTC;Prisma适配器与Worker已有明确UTC处理,不能把临时脚本的本地解析偏移误判成数据偏移。
- 首轮真实浏览器:5个监控页签×3尺寸=15项通过,额外规则和通知页三尺寸/刷新通过,无页面异常。真实API规则、摘要、rows、overview均200(样本1139ms,低负载单次结果不等于性能P95验收),非法page=0返回400。规则API仍返回空集合,页面下限/最低样本空、启用未勾选。
- 临时通知验收使用两个已有企业下的新客户端用户,以及全新draft、无应用/无路由的QA签名和rejected引流;仅引用既有全国通道,不改变企业、余额和通道配置,不发送短信。真实报备任务触发器产生签名/引流各1事件;双端实际消息和详情三尺寸已显示,进一步已读/追加/隔离与清理在最终节记录。
- 界面收尾:运行概况去除重复主标题,状态记录改用既有Tabs;消息详情分页失败就地显示,重试先清错误,以请求序号忽略关闭后的迟到结果,防止标记已读尚未返回时关闭弹窗后又被重新打开。新增关闭期间异步返回的回归测试。前端20套100项通过,类型检查/构建及格式、ESLint、CSS门禁通过;TypeScript曾发现新测试的exact/.at不符合项目类型目标,已修正后复验。API/Gateway无新增改动,沿用638项与Go全量结果。
+14 -22
View File
@@ -12,6 +12,7 @@ import {
Pagination, Pagination,
Select, Select,
Table, Table,
Tabs,
Tag, Tag,
type DateRangeValue, type DateRangeValue,
type TableColumn, type TableColumn,
@@ -150,29 +151,20 @@ function RecordDetailModal({ record, onClose }: { record: ReportRecord; onClose:
export function AdminReportRecordsPage() { export function AdminReportRecordsPage() {
const [params, setParams] = useSearchParams(); const [params, setParams] = useSearchParams();
const readiness = params.get('tab') === 'readiness';
return ( return (
<section className="page-stack"> <Tabs
<div className="page-heading" role="tablist" aria-label="状态记录类型"> value={params.get('tab') === 'readiness' ? 'readiness' : 'records'}
<Button onChange={(value) => {
role="tab" const next = new URLSearchParams(params);
aria-selected={!readiness} if (value === 'readiness') next.set('tab', value);
variant={readiness ? 'secondary' : 'primary'} else next.delete('tab');
onClick={() => setParams({})} setParams(next);
> }}
items={[
</Button> { label: '状态记录', value: 'records', content: <ReportStatusRecords /> },
<Button { label: '报备状态变化消息', value: 'readiness', content: <ReportNotificationsPage portal="admin" /> },
role="tab" ]}
aria-selected={readiness} />
variant={readiness ? 'primary' : 'secondary'}
onClick={() => setParams({ tab: 'readiness' })}
>
</Button>
</div>
{readiness ? <ReportNotificationsPage portal="admin" /> : <ReportStatusRecords />}
</section>
); );
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Activity } from 'lucide-react'; import { Activity } from 'lucide-react';
import { Breadcrumb, Button, CarrierTag, Table, Tag, type TableColumn } from '@/components/ui'; import { Button, CarrierTag, Table, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type AdminChannel } from '@/api/adminApi'; import { adminApi, type AdminChannel } from '@/api/adminApi';
const columns: Array<TableColumn<AdminChannel>> = [ const columns: Array<TableColumn<AdminChannel>> = [
@@ -77,7 +77,7 @@ export function MonitorRuntimeOverview() {
<section className="page-stack"> <section className="page-stack">
<div className="page-heading"> <div className="page-heading">
<div> <div>
<Breadcrumb items={['发送监控']} /> <h2></h2>
</div> </div>
<Button <Button
icon={<Activity size={16} />} icon={<Activity size={16} />}
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react'; import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { beforeEach, expect, it, vi } from 'vitest'; import { beforeEach, expect, it, vi } from 'vitest';
import { ReportNotificationsPage } from './ReportNotificationsPage'; import { ReportNotificationsPage } from './ReportNotificationsPage';
@@ -57,3 +57,21 @@ it('reports a failed read without pretending success', async () => {
expect(await screen.findByRole('alert')).toHaveTextContent('标记失败'); expect(await screen.findByRole('alert')).toHaveTextContent('标记失败');
expect(screen.getByRole('button', { name: '标记已读' })).toBeEnabled(); expect(screen.getByRole('button', { name: '标记已读' })).toBeEnabled();
}); });
it('does not reopen a closed dialog when the read response arrives later', async () => {
const user = userEvent.setup();
render(<ReportNotificationsPage />);
await user.click(await screen.findByRole('button', { name: '查看消息' }));
let finish!: (result: unknown) => void;
api.request.mockImplementationOnce(
() =>
new Promise((resolve) => {
finish = resolve;
}),
);
await user.click(await screen.findByRole('button', { name: '标记已读' }));
const closeButtons = screen.getAllByRole('button', { name: /^关闭$/ });
await user.click(closeButtons[closeButtons.length - 1]);
await act(async () => finish({ success: true }));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
});
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { Breadcrumb, Button, Modal, Table, Tag, type TableColumn } from '@/components/ui'; import { Breadcrumb, Button, Modal, Table, Tag, type TableColumn } from '@/components/ui';
import { request } from '@/api/core/httpClient'; import { request } from '@/api/core/httpClient';
import './report-notifications.css'; import './report-notifications.css';
@@ -32,6 +32,12 @@ export function ReportNotificationsPage({ portal = 'client' }: { portal?: 'admin
const [detail, setDetail] = useState<{ hour: Hour; items: Event[]; total: number; page: number } | null>(null); const [detail, setDetail] = useState<{ hour: Hour; items: Event[]; total: number; page: number } | null>(null);
const [detailError, setDetailError] = useState(''); const [detailError, setDetailError] = useState('');
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const detailRequest = useRef(0);
const closeDetail = () => {
detailRequest.current++;
setDetail(null);
setBusy(false);
};
const base = `/${portal}/report-notifications`; const base = `/${portal}/report-notifications`;
const load = useCallback( const load = useCallback(
async (signal?: AbortSignal) => { async (signal?: AbortSignal) => {
@@ -54,32 +60,40 @@ export function ReportNotificationsPage({ portal = 'client' }: { portal?: 'admin
return () => controller.abort(); return () => controller.abort();
}, [load]); }, [load]);
async function open(item: Hour, detailPage = 1) { async function open(item: Hour, detailPage = 1) {
const sequence = ++detailRequest.current;
setBusy(true); setBusy(true);
setDetailError(''); setDetailError('');
try { try {
const result = await request<{ hour: Hour } & Page<Event>>(`${base}/${item.id}?page=${detailPage}`); const result = await request<{ hour: Hour } & Page<Event>>(`${base}/${item.id}?page=${detailPage}`);
if (sequence !== detailRequest.current) return;
setDetail({ ...result, hour: { ...result.hour, unread: item.unread } }); setDetail({ ...result, hour: { ...result.hour, unread: item.unread } });
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : '详情加载失败'); if (sequence !== detailRequest.current) return;
const message = e instanceof Error ? e.message : '详情加载失败';
if (detail) setDetailError(message);
else setError(message);
} finally { } finally {
setBusy(false); if (sequence === detailRequest.current) setBusy(false);
} }
} }
async function markRead() { async function markRead() {
if (!detail) return; if (!detail) return;
const sequence = ++detailRequest.current;
setDetailError('');
setBusy(true); setBusy(true);
try { try {
await request(`${base}/${detail.hour.id}/read`, { await request(`${base}/${detail.hour.id}/read`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ revision: detail.hour.revision }), body: JSON.stringify({ revision: detail.hour.revision }),
}); });
setDetail({ ...detail, hour: { ...detail.hour, unread: false } }); if (sequence === detailRequest.current)
setDetail((current) => (current ? { ...current, hour: { ...current.hour, unread: false } } : null));
window.dispatchEvent(new Event('cmpp-report-notification-refresh')); window.dispatchEvent(new Event('cmpp-report-notification-refresh'));
await load(); await load();
} catch (e) { } catch (e) {
setDetailError(e instanceof Error ? e.message : '标记已读失败'); if (sequence === detailRequest.current) setDetailError(e instanceof Error ? e.message : '标记已读失败');
} finally { } finally {
setBusy(false); if (sequence === detailRequest.current) setBusy(false);
} }
} }
const columns: TableColumn<Hour>[] = [ const columns: TableColumn<Hour>[] = [
@@ -163,10 +177,10 @@ export function ReportNotificationsPage({ portal = 'client' }: { portal?: 'admin
open open
title="报备状态变化消息" title="报备状态变化消息"
size="xl" size="xl"
onClose={() => setDetail(null)} onClose={closeDetail}
footer={ footer={
<> <>
<Button variant="secondary" onClick={() => setDetail(null)}> <Button variant="secondary" onClick={closeDetail}>
</Button> </Button>
<Button disabled={busy || !detail.hour.unread} onClick={() => void markRead()}> <Button disabled={busy || !detail.hour.unread} onClick={() => void markRead()}>