feat: 增强下游重投与签名质量检测

This commit is contained in:
hectorzhao
2026-08-12 17:05:53 +08:00
parent 1d8d6701a6
commit 4994841709
23 changed files with 960 additions and 67 deletions
+12 -1
View File
@@ -1,5 +1,5 @@
import { request, requestBlob, requestForm, withQuery } from '../core/httpClient';
import type { BatchRequeueResponse, BatchTaskMessagePage, DailyProfitReport, DailyQualityReport, DailyReconciliationReport, DashboardResponse, DownstreamDeliveryDashboard, DownstreamDeliveryRecord, DownstreamRecoveryStatusExportQuery, DownstreamRecoveryStatusResponse, GatewayDownstreamRecoveryStatus, GatewaySubmitException, GatewaySubmitExceptionResponse, OperationLogResponse, PagedResponse, PagedResult, PendingAuditCounts, ProfitReportSummary, ProtocolInteractionLogResponse, QualityReportSummary, ReceiptAnomalyResponse, ReconciliationReportSummary, SendQualityResponse, SignatureChannelQualityResponse, SmsBatchTask, SmsMessageRecord, SmsMessageSegmentAudit, SmsUplinkMessage, SystemLogExportResult } from '../types';
import type { BatchRequeueResponse, BatchTaskMessagePage, DailyProfitReport, DailyQualityReport, DailyReconciliationReport, DashboardResponse, DownstreamDeliveryDashboard, DownstreamDeliveryRecord, DownstreamRecoveryStatusExportQuery, DownstreamRecoveryStatusResponse, DownstreamRequeueFilter, DownstreamRequeuePreview, DownstreamRequeueTask, GatewayDownstreamRecoveryStatus, GatewaySubmitException, GatewaySubmitExceptionResponse, OperationLogResponse, PagedResponse, PagedResult, PendingAuditCounts, ProfitReportSummary, ProtocolInteractionLogResponse, QualityReportSummary, ReceiptAnomalyResponse, ReconciliationReportSummary, SendQualityResponse, SignatureChannelQualityResponse, SmsBatchTask, SmsMessageRecord, SmsMessageSegmentAudit, SmsUplinkMessage, SystemLogExportResult } from '../types';
// Read-heavy operations endpoints are isolated from configuration mutations.
export const adminOperationsApi = {
@@ -53,6 +53,8 @@ export const adminOperationsApi = {
request<GatewaySubmitExceptionResponse>(withQuery('/admin/operations/gateway-submit-dead-letters', query)),
requeueGatewaySubmitException: (id: string, body: { confirmedNotSubmitted: boolean; reason: string }) =>
request<GatewaySubmitException>(`/admin/operations/gateway-submit-dead-letters/${id}/requeue`, { method: 'POST', body: JSON.stringify(body) }),
resolveGatewaySubmitException: (id: string) =>
request<GatewaySubmitException>(`/admin/operations/gateway-submit-dead-letters/${id}/resolve`, { method: 'POST', body: JSON.stringify({}) }),
listReceiptAnomalies: (query: { tenantId?: string; applicationId?: string; channelId?: string; anomalyType?: string; status?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
request<ReceiptAnomalyResponse>(withQuery('/admin/operations/receipt-anomalies', query)),
listStatistics: (query: { tenantId?: string; groupBy?: string } = {}) => request<Array<Record<string, unknown>>>(withQuery('/admin/operations/statistics', query)),
@@ -70,4 +72,13 @@ export const adminOperationsApi = {
request<DownstreamDeliveryRecord>(`/admin/operations/downstream-deliveries/${id}/requeue`, { method: 'POST', body: JSON.stringify({}) }),
batchRequeueDownstreamDeliveries: (ids: string[]) =>
request<BatchRequeueResponse>('/admin/operations/downstream-deliveries/requeue', { method: 'POST', body: JSON.stringify({ ids }) }),
previewDownstreamRequeueTask: (filter: DownstreamRequeueFilter) =>
request<DownstreamRequeuePreview>('/admin/operations/downstream-requeue-tasks/preview', { method: 'POST', body: JSON.stringify({ filter }) }),
createDownstreamRequeueTask: (body: { filter: DownstreamRequeueFilter; snapshotAt: string; reason: string; ratePerSecond: number; consecutiveFailureLimit: number }) =>
request<DownstreamRequeueTask>('/admin/operations/downstream-requeue-tasks', { method: 'POST', body: JSON.stringify(body) }),
listDownstreamRequeueTasks: (query: { status?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResponse<DownstreamRequeueTask>>(withQuery('/admin/operations/downstream-requeue-tasks', query)),
getDownstreamRequeueTask: (id: string) => request<DownstreamRequeueTask>(`/admin/operations/downstream-requeue-tasks/${id}`),
changeDownstreamRequeueTaskStatus: (id: string, action: 'pause' | 'resume' | 'terminate') =>
request<DownstreamRequeueTask>(`/admin/operations/downstream-requeue-tasks/${id}/${action}`, { method: 'POST', body: JSON.stringify({}) }),
};
+45
View File
@@ -483,6 +483,51 @@ export type BatchRequeueResponse = {
results: Array<{ id: string; status: 'success' | 'failed'; errorMessage?: string }>;
};
export type DownstreamRequeueFilter = {
tenantId?: string;
applicationId?: string;
deliveryType?: string;
status?: string;
keyword?: string;
createdAtFrom?: string;
createdAtTo?: string;
};
export type DownstreamRequeuePreview = {
snapshotAt: string;
matchedCount: number;
replayableCount: number;
skippedCount: number;
applicationCount: number;
oldestCreatedAt?: string | null;
statusCounts: Record<string, number>;
filter: DownstreamRequeueFilter;
};
export type DownstreamRequeueTask = {
id: string;
taskNo: string;
status: string;
filterSnapshot: DownstreamRequeueFilter;
snapshotAt: string;
reason: string;
ratePerSecond: number;
totalCount: number;
successCount: number;
failedCount: number;
skippedCount: number;
waitingCount: number;
lastError?: string | null;
createdAt: string;
startedAt?: string | null;
finishedAt?: string | null;
tenant?: TenantOption | null;
application?: EnterpriseApplication | null;
createdBy?: { id: string; displayName: string; username: string } | null;
itemCounts?: Record<string, number>;
recentItems?: Array<{ id: string; status: string; skipReason?: string | null; errorMessage?: string | null; delivery: { messageId?: string | null; deliveryType: string; status: string; lastError?: string | null } }>;
};
export type DownstreamDeliveryDashboard = {
summary: {
total: number;
+2 -2
View File
@@ -407,7 +407,7 @@ function UnreportedSignaturesCard({
<div className="signature-quality-card__heading">
<div>
<div className="section-heading__title"><h2></h2><Tag tone="warning"></Tag></div>
<p className="muted">{data?.date ?? '所选日期'} </p>
<p className="muted">{data?.date ?? '所选日期'} </p>
</div>
<div className="signature-quality-card__query">
<Input
@@ -420,7 +420,7 @@ function UnreportedSignaturesCard({
<Button disabled={loading} icon={<Search size={16} />} onClick={onSearch} variant="secondary"></Button>
</div>
</div>
<div className="signature-quality-card__note"><strong></strong></div>
<div className="signature-quality-card__note"><strong></strong></div>
<Table
columns={columns}
data={data?.items ?? []}
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AlertTriangle, BarChart3, CheckCircle2, Eye, RefreshCw, Search, TimerReset } from 'lucide-react';
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type EnterpriseApplication } from '@/api/adminApi';
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type EnterpriseApplication } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
@@ -153,7 +153,7 @@ export function AdminDownstreamDeliveriesPage() {
const [deliveryType, setDeliveryType] = useState('all');
const [applicationId, setApplicationId] = useState('all');
const [page, setPage] = useState(1);
const [pageSize] = useState(10);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
@@ -164,6 +164,28 @@ export function AdminDownstreamDeliveriesPage() {
const [requeueBusy, setRequeueBusy] = useState(false);
const [requeueResult, setRequeueResult] = useState<RequeueResult | null>(null);
const requeueInFlightRef = useRef(false);
const [taskPreview, setTaskPreview] = useState<DownstreamRequeuePreview | null>(null);
const [taskPreviewBusy, setTaskPreviewBusy] = useState(false);
const [taskReason, setTaskReason] = useState('');
const [taskRate, setTaskRate] = useState(10);
const [taskCreateBusy, setTaskCreateBusy] = useState(false);
const [requeueTasks, setRequeueTasks] = useState<DownstreamRequeueTask[]>([]);
const [selectedTask, setSelectedTask] = useState<DownstreamRequeueTask | null>(null);
const currentTaskFilter = useCallback(() => ({
keyword: keyword || undefined,
status,
deliveryType,
applicationId,
createdAtFrom: dateRange.start,
createdAtTo: dateRange.end,
}), [applicationId, dateRange.end, dateRange.start, deliveryType, keyword, status]);
const loadRequeueTasks = useCallback(() => {
adminApi.listDownstreamRequeueTasks({ page: 1, pageSize: 10 })
.then((response) => setRequeueTasks(response.items))
.catch(() => undefined);
}, []);
const loadData = useCallback(() => {
setLoading(true);
@@ -202,6 +224,39 @@ export function AdminDownstreamDeliveriesPage() {
loadData();
}, [loadData]);
useEffect(() => {
loadRequeueTasks();
const timer = window.setInterval(loadRequeueTasks, 3000);
return () => window.clearInterval(timer);
}, [loadRequeueTasks]);
const openTaskPreview = async () => {
setTaskPreviewBusy(true);
setError('');
try {
setTaskPreview(await adminApi.previewDownstreamRequeueTask(currentTaskFilter()));
setTaskReason('');
} catch (failure) {
setError(failure instanceof Error ? failure.message : '后台重投范围预检失败');
} finally {
setTaskPreviewBusy(false);
}
};
const createRequeueTask = async () => {
if (!taskPreview || taskReason.trim().length < 5) return;
setTaskCreateBusy(true);
try {
await adminApi.createDownstreamRequeueTask({ filter: taskPreview.filter, snapshotAt: taskPreview.snapshotAt, reason: taskReason.trim(), ratePerSecond: taskRate, consecutiveFailureLimit: 10 });
setTaskPreview(null);
loadRequeueTasks();
} catch (failure) {
setError(failure instanceof Error ? failure.message : '后台重投任务创建失败');
} finally {
setTaskCreateBusy(false);
}
};
const replayableStatuses = useMemo(() => new Set(['pending', 'delivered', 'failed', 'unconfirmed', 'rejected']), []);
const selectableIds = useMemo(
() => records.filter((item) => replayableStatuses.has(item.status)).map((item) => item.id),
@@ -455,6 +510,17 @@ export function AdminDownstreamDeliveriesPage() {
<strong>{selectedIds.length}</strong>
</p>
<div>
<label className="downstream-page-size">
<select value={pageSize} onChange={(event) => { setPageSize(Number(event.target.value)); setPage(1); setSelectedIds([]); }}>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</label>
<Button disabled={taskPreviewBusy} onClick={() => void openTaskPreview()} variant="secondary">
{taskPreviewBusy ? '范围预检中…' : '按筛选条件重投'}
</Button>
<Button
disabled={selectableIds.length === 0}
onClick={() => setSelectedIds(allSelected ? [] : selectableIds)}
@@ -548,7 +614,62 @@ export function AdminDownstreamDeliveriesPage() {
/>
</div>
<div className="surface admin-task-table-card report-task-table-card">
<div className="section-heading">
<div><h2></h2><p className="muted"></p></div>
<Button onClick={loadRequeueTasks} variant="ghost"></Button>
</div>
<div className="downstream-requeue-task-list">
{requeueTasks.map((task) => (
<article key={task.id}>
<div><strong>{task.taskNo}</strong><span>{formatDateTime(task.createdAt)}</span></div>
<div><span>{task.application?.name ?? '多个应用'}</span><small>{task.reason}</small></div>
<div><Tag tone={task.status === 'completed' ? 'success' : task.status === 'paused' ? 'warning' : task.status === 'partial_completed' ? 'danger' : 'info'}>{task.status}</Tag><span>{task.successCount + task.failedCount + task.skippedCount}/{task.totalCount}</span></div>
<div className="downstream-requeue-task-list__actions">
<Button onClick={() => void adminApi.getDownstreamRequeueTask(task.id).then(setSelectedTask)} size="sm" variant="ghost"></Button>
{task.status === 'running' || task.status === 'queued' ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'pause').then(loadRequeueTasks)} size="sm" variant="secondary"></Button> : null}
{task.status === 'paused' ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'resume').then(loadRequeueTasks)} size="sm" variant="secondary"></Button> : null}
{['queued', 'running', 'paused'].includes(task.status) ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'terminate').then(loadRequeueTasks)} size="sm" variant="warning"></Button> : null}
</div>
</article>
))}
{requeueTasks.length === 0 ? <p className="muted"></p> : null}
</div>
</div>
{detail ? <DeliveryDetailModal record={detail} onClose={() => setDetail(null)} /> : null}
{taskPreview ? (
<Modal open onClose={() => !taskCreateBusy && setTaskPreview(null)} title="创建下游后台重投任务" size="xl" footer={<><Button disabled={taskCreateBusy} onClick={() => setTaskPreview(null)} variant="ghost"></Button><Button disabled={taskCreateBusy || taskPreview.replayableCount === 0 || taskReason.trim().length < 5} onClick={() => void createRequeueTask()} variant="warning">{taskCreateBusy ? '创建中…' : '确认创建任务'}</Button></>}>
<div className="downstream-requeue-preview">
<div className="detail-grid">
<div><span></span><strong>{taskPreview.matchedCount}</strong></div>
<div><span></span><strong>{taskPreview.replayableCount}</strong></div>
<div><span></span><strong>{taskPreview.skippedCount}</strong></div>
<div><span></span><strong>{taskPreview.applicationCount}</strong></div>
<div><span></span><strong>{taskPreview.oldestCreatedAt ? formatDateTime(taskPreview.oldestCreatedAt) : '-'}</strong></div>
<div><span></span><strong>{formatDateTime(taskPreview.snapshotAt)}</strong></div>
</div>
<p className="muted">{Object.entries(taskPreview.statusCounts).map(([key, value]) => `${statusLabel[key] ?? key} ${value}`).join('') || '无'}</p>
<Select label="执行速度" value={String(taskRate)} onChange={(event) => setTaskRate(Number(event.target.value))} options={[{ label: '平稳(每应用10条/秒)', value: '10' }, { label: '快速(每应用20条/秒)', value: '20' }, { label: '低速(每应用5条/秒)', value: '5' }]} />
<label className="field"><span> *</span><textarea value={taskReason} onChange={(event) => setTaskReason(event.target.value)} placeholder="请填写事故原因、工单号或处理说明(至少5个字)" rows={3} /></label>
<div className="downstream-requeue-warning"><AlertTriangle size={20} /><strong></strong></div>
</div>
</Modal>
) : null}
{selectedTask ? (
<Modal open onClose={() => setSelectedTask(null)} title={`后台重投任务 ${selectedTask.taskNo}`} size="xl" footer={<Button onClick={() => setSelectedTask(null)}></Button>}>
<div className="report-record-detail">
<div className="detail-grid">
<div><span></span><strong>{selectedTask.status}</strong></div><div><span></span><strong>{selectedTask.reason}</strong></div>
<div><span></span><strong>{selectedTask.totalCount}</strong></div><div><span></span><strong>{selectedTask.successCount}</strong></div>
<div><span></span><strong>{selectedTask.failedCount}</strong></div><div><span></span><strong>{selectedTask.skippedCount}</strong></div>
<div><span></span><strong>{selectedTask.waitingCount}</strong></div><div><span></span><strong>{selectedTask.ratePerSecond}/</strong></div>
</div>
<h3></h3>
<div className="downstream-requeue-detail-items">{(selectedTask.recentItems ?? []).map((item) => <div key={item.id}><span>{item.delivery.messageId ?? '-'}</span><Tag tone={item.status === 'success' ? 'success' : item.status === 'failed' ? 'danger' : item.status === 'skipped' ? 'warning' : 'info'}>{item.status}</Tag><span>{item.skipReason ?? item.errorMessage ?? '-'}</span></div>)}</div>
</div>
</Modal>
) : null}
{requeueTarget ? (
<Modal
footer={requeueResult ? (
@@ -18,6 +18,13 @@ const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'd
resolved: 'success',
};
const resolvedStatusLabel: Record<string, string> = {
manually_resolved: '人工标记已处理',
accepted: '上游已受理',
rejected: '上游已拒绝',
timeout: '上游提交超时',
};
function formatTime(value?: string | null) {
if (!value) return '-';
const date = new Date(value);
@@ -34,10 +41,11 @@ function commandValue(record: GatewaySubmitException, key: string) {
return value == null ? '' : String(value);
}
function ExceptionDetailModal({ record, onClose, onRequestRequeue }: {
function ExceptionDetailModal({ record, onClose, onRequestRequeue, onRequestResolve }: {
record: GatewaySubmitException;
onClose: () => void;
onRequestRequeue: () => void;
onRequestResolve: () => void;
}) {
const phone = record.messageState?.phoneNumber ?? commandValue(record, 'phoneNumber');
const content = record.messageState?.content ?? commandValue(record, 'content');
@@ -50,6 +58,7 @@ function ExceptionDetailModal({ record, onClose, onRequestRequeue }: {
footer={(
<div className="modal-footer-actions">
<Button onClick={onClose} variant="ghost"></Button>
{record.status === 'pending' ? <Button icon={<CheckCircle2 size={15} />} onClick={onRequestResolve} variant="secondary"></Button> : null}
{record.status === 'pending' ? <Button icon={<RotateCcw size={15} />} onClick={onRequestRequeue}></Button> : null}
</div>
)}
@@ -76,7 +85,7 @@ function ExceptionDetailModal({ record, onClose, onRequestRequeue }: {
<div><span></span><strong>{formatTime(record.createdAt)}</strong></div>
<div><span></span><strong>{formatTime(record.lastRetriedAt)}</strong></div>
<div><span></span><strong>{formatTime(record.resolvedAt)}</strong></div>
<div><span></span><strong>{record.resolvedStatus ?? '-'}</strong></div>
<div><span></span><strong>{record.resolvedStatus ? (resolvedStatusLabel[record.resolvedStatus] ?? record.resolvedStatus) : '-'}</strong></div>
<div className="detail-grid__wide"><span></span><strong>{content || '-'}</strong></div>
<div className="detail-grid__wide"><span></span><strong>{record.failureCode}</strong></div>
<div className="detail-grid__wide"><span></span><strong>{record.failureMessage}</strong></div>
@@ -90,6 +99,39 @@ function ExceptionDetailModal({ record, onClose, onRequestRequeue }: {
);
}
function ResolveModal({ record, submitting, onClose, onSubmit }: {
record: GatewaySubmitException;
submitting: boolean;
onClose: () => void;
onSubmit: () => void;
}) {
return (
<Modal
open
onClose={onClose}
size="md"
title="确认标记为已处理"
footer={(
<div className="modal-footer-actions">
<Button disabled={submitting} onClick={onClose} variant="ghost"></Button>
<Button disabled={submitting} icon={<CheckCircle2 size={15} />} onClick={onSubmit}>
{submitting ? '处理中...' : '确认已处理'}
</Button>
</div>
)}
>
<div className="downstream-requeue-confirm">
<CheckCircle2 aria-hidden="true" size={24} />
<div>
<p></p>
<small>{record.messageId ?? record.streamMessageId}</small>
<strong></strong>
</div>
</div>
</Modal>
);
}
function RequeueModal({ record, submitting, onClose, onSubmit }: {
record: GatewaySubmitException;
submitting: boolean;
@@ -156,6 +198,8 @@ function GatewaySubmitExceptionPanel() {
const [error, setError] = useState('');
const [detail, setDetail] = useState<GatewaySubmitException | null>(null);
const [requeueRecord, setRequeueRecord] = useState<GatewaySubmitException | null>(null);
const [resolveRecord, setResolveRecord] = useState<GatewaySubmitException | null>(null);
const [resolving, setResolving] = useState(false);
const pageSize = 10;
const loadData = useCallback(() => {
@@ -192,7 +236,7 @@ function GatewaySubmitExceptionPanel() {
{ key: 'failure', title: '异常原因', render: (record) => <div><strong>{record.failureCode}</strong><small className="table-cell-note">{record.failureMessage}</small></div> },
{ key: 'attempts', title: '尝试', width: '80px', align: 'center', render: (record) => `${record.attempts}/${record.maxAttempts}` },
{ key: 'status', title: '状态', width: '120px', render: (record) => <Tag tone={statusTone[record.status] ?? 'neutral'}>{statusLabel[record.status] ?? record.status}</Tag> },
{ key: 'actions', title: '操作', width: '105px', align: 'right', render: (record) => <Button icon={<Eye size={14} />} onClick={() => setDetail(record)} size="sm" variant="ghost"></Button> },
{ key: 'actions', title: '操作', width: '190px', align: 'right', render: (record) => <div className="table-actions"><Button icon={<Eye size={14} />} onClick={() => setDetail(record)} size="sm" variant="ghost"></Button>{record.status === 'pending' ? <Button icon={<CheckCircle2 size={14} />} onClick={() => setResolveRecord(record)} size="sm" variant="secondary"></Button> : null}</div> },
], []);
async function submitRequeue(reason: string) {
@@ -214,6 +258,22 @@ function GatewaySubmitExceptionPanel() {
}
}
async function submitResolve() {
if (!resolveRecord) return;
setResolving(true);
try {
await adminApi.resolveGatewaySubmitException(resolveRecord.id);
setResolveRecord(null);
setDetail(null);
setError('');
loadData();
} catch (failure) {
setError(failure instanceof Error ? failure.message : '标记已处理失败');
} finally {
setResolving(false);
}
}
const totalPages = Math.max(1, Math.ceil(total / pageSize));
return (
<div className="page-stack admin-sms-task-page report-record-page gateway-exception-page">
@@ -243,8 +303,9 @@ function GatewaySubmitExceptionPanel() {
<Table columns={columns} data={items} emptyText={loading ? '加载中...' : '暂无提交异常'} pagination={false} rowKey="id" />
<Pagination total={total} page={page} totalPages={totalPages} onPageChange={setPage} previousDisabled={page <= 1} nextDisabled={page >= totalPages} onPrevious={() => setPage((current) => Math.max(1, current - 1))} onNext={() => setPage((current) => Math.min(totalPages, current + 1))} />
</div>
{detail ? <ExceptionDetailModal record={detail} onClose={() => setDetail(null)} onRequestRequeue={() => setRequeueRecord(detail)} /> : null}
{detail ? <ExceptionDetailModal record={detail} onClose={() => setDetail(null)} onRequestRequeue={() => setRequeueRecord(detail)} onRequestResolve={() => setResolveRecord(detail)} /> : null}
{requeueRecord ? <RequeueModal record={requeueRecord} submitting={submitting} onClose={() => setRequeueRecord(null)} onSubmit={submitRequeue} /> : null}
{resolveRecord ? <ResolveModal record={resolveRecord} submitting={resolving} onClose={() => setResolveRecord(null)} onSubmit={() => void submitResolve()} /> : null}
</div>
);
}
+11
View File
@@ -9426,3 +9426,14 @@
font-size: var(--font-size-base);
font-variant-numeric: tabular-nums;
}
.downstream-page-size { display: inline-flex; align-items: center; gap: 8px; color: var(--text-muted); font-size: 13px; }
.downstream-page-size select { min-width: 76px; height: 36px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); padding: 0 10px; }
.downstream-requeue-task-list { display: grid; gap: 10px; }
.downstream-requeue-task-list article { display: grid; grid-template-columns: minmax(170px, 1fr) minmax(220px, 2fr) minmax(150px, .8fr) auto; gap: 16px; align-items: center; padding: 14px 0; border-top: 1px solid var(--border); }
.downstream-requeue-task-list article > div { display: grid; gap: 4px; }
.downstream-requeue-task-list__actions { display: flex !important; flex-wrap: wrap; justify-content: flex-end; }
.downstream-requeue-preview { display: grid; gap: 18px; }
.downstream-requeue-warning { display: flex; align-items: flex-start; gap: 10px; padding: 12px; border-radius: 10px; background: var(--warning-soft, #fff7ed); color: var(--warning-text, #9a3412); }
.downstream-requeue-detail-items { display: grid; gap: 8px; }
.downstream-requeue-detail-items > div { display: grid; grid-template-columns: minmax(180px, 1fr) 120px minmax(220px, 2fr); gap: 12px; align-items: center; padding: 10px 0; border-top: 1px solid var(--border); }
@media (max-width: 900px) { .downstream-requeue-task-list article { grid-template-columns: 1fr; } .downstream-requeue-task-list__actions { justify-content: flex-start; } .downstream-requeue-detail-items > div { grid-template-columns: 1fr; } }