This commit is contained in:
@@ -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:标记已读或明细分页请求未返回时关闭弹窗,随后迟到成功/失败;弹窗保持关闭,不被旧请求重新打开。分页失败在弹窗内展示,成功重试清除旧错误。
|
||||||
|
|||||||
@@ -4618,3 +4618,12 @@ git diff --check
|
|||||||
- 测试机19:05~19:08基线:旧部署442dda7;短信119509、近72小时尝试0、待匹配回执0;提交命令/结果Stream pending/lag均0;数据库约2363MB、连接13/100、根盘可用24GB。Gateway desired=6、connected=0是发布前既有状态,不宣称供应商链路通过。当前没有真实高峰/长短信负载;500TPS性能预算及故障演练尚未执行,不作容量承诺。
|
- 测试机19:05~19: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(样本11~39ms,低负载单次结果不等于性能P95验收),非法page=0返回400。规则API仍返回空集合,页面下限/最低样本空、启用未勾选。
|
||||||
|
- 临时通知验收使用两个已有企业下的新客户端用户,以及全新draft、无应用/无路由的QA签名和rejected引流;仅引用既有全国通道,不改变企业、余额和通道配置,不发送短信。真实报备任务触发器产生签名/引流各1事件;双端实际消息和详情三尺寸已显示,进一步已读/追加/隔离与清理在最终节记录。
|
||||||
|
- 界面收尾:运行概况去除重复主标题,状态记录改用既有Tabs;消息详情分页失败就地显示,重试先清错误,以请求序号忽略关闭后的迟到结果,防止标记已读尚未返回时关闭弹窗后又被重新打开。新增关闭期间异步返回的回归测试。前端20套100项通过,类型检查/构建及格式、ESLint、CSS门禁通过;TypeScript曾发现新测试的exact/.at不符合项目类型目标,已修正后复验。API/Gateway无新增改动,沿用638项与Go全量结果。
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()}>
|
||||||
|
|||||||
Reference in New Issue
Block a user