feat(client): add digital sync retry details
This commit is contained in:
@@ -1211,11 +1211,15 @@
|
||||
display: grid; gap: 6px; margin: -4px 0 10px;
|
||||
}
|
||||
.de-management-sync-failure {
|
||||
width: 100%; font-family: inherit; text-align: left; cursor: pointer;
|
||||
display: grid; grid-template-columns: 56px 54px minmax(54px, 0.7fr) minmax(0, 1.6fr);
|
||||
align-items: center; gap: 8px; min-height: 28px; padding: 6px 9px;
|
||||
border-radius: 10px; border: 1px solid rgba(224,72,72,0.14);
|
||||
background: rgba(224,72,72,0.055); color: #7b4450; font-size: 11px;
|
||||
}
|
||||
.de-management-sync-failure:hover {
|
||||
border-color: rgba(224,72,72,0.28); box-shadow: 0 0 0 2px rgba(224,72,72,0.06);
|
||||
}
|
||||
.de-management-sync-failure span,
|
||||
.de-management-sync-failure em,
|
||||
.de-management-sync-failure small {
|
||||
|
||||
@@ -71,6 +71,8 @@ interface QimingclawSyncFailure {
|
||||
operation: string;
|
||||
attempts: number;
|
||||
lastAttemptAt: string | null;
|
||||
nextRetryAt?: string | null;
|
||||
dueForRetry?: boolean;
|
||||
error: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -392,7 +394,12 @@ function compactSyncText(value?: string | null): string {
|
||||
function syncFailureLine(failure: QimingclawSyncFailure): string {
|
||||
const error = compactSyncText(failure.error);
|
||||
const detail = error.length > 120 ? `${error.slice(0, 120)}...` : error;
|
||||
return `${failure.entityType}/${failure.operation} ${failure.id} 重试 ${failure.attempts} 次${detail ? `,${detail}` : ''}`;
|
||||
const retryText = failure.dueForRetry
|
||||
? '等待补偿'
|
||||
: failure.nextRetryAt
|
||||
? `下次重试 ${failure.nextRetryAt}`
|
||||
: '等待重试';
|
||||
return `${failure.entityType}/${failure.operation} ${failure.id} 重试 ${failure.attempts} 次,${retryText}${detail ? `,${detail}` : ''}`;
|
||||
}
|
||||
|
||||
function withRuntimePlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): DebugPlan {
|
||||
|
||||
@@ -71,6 +71,8 @@ interface ManagementSyncFailure {
|
||||
operation: string;
|
||||
attempts: number;
|
||||
lastAttemptAt: string | null;
|
||||
nextRetryAt?: string | null;
|
||||
dueForRetry?: boolean;
|
||||
error: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -478,6 +480,7 @@ function syncFailureSummary(failure: ManagementSyncFailure): string {
|
||||
return [
|
||||
`${entityTypeLabel(failure.entityType)} / ${failure.operation}`,
|
||||
`重试 ${failure.attempts} 次`,
|
||||
failure.dueForRetry ? '等待补偿' : `下次 ${syncFailureRetryTime(failure)}`,
|
||||
error,
|
||||
].filter(Boolean).join(' · ');
|
||||
}
|
||||
@@ -486,6 +489,11 @@ function syncFailureTime(failure: ManagementSyncFailure): string {
|
||||
return formatBeijingDateTime(failure.lastAttemptAt || failure.updatedAt || failure.createdAt) || '未尝试';
|
||||
}
|
||||
|
||||
function syncFailureRetryTime(failure: ManagementSyncFailure): string {
|
||||
if (failure.dueForRetry) return '可重试';
|
||||
return formatBeijingDateTime(failure.nextRetryAt) || '待计算';
|
||||
}
|
||||
|
||||
async function readManagementSyncStatus(): Promise<ManagementSyncStatus | null> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.getSyncStatus) return null;
|
||||
@@ -662,6 +670,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
const [scheduleTime, setScheduleTime] = useState('09:00');
|
||||
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
||||
const [reportPreviewOpen, setReportPreviewOpen] = useState(false);
|
||||
const [selectedSyncFailure, setSelectedSyncFailure] = useState<ManagementSyncFailure | null>(null);
|
||||
const [profileForm, setProfileForm] = useState({
|
||||
name: '',
|
||||
company: '',
|
||||
@@ -1374,12 +1383,18 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
{managementSyncFailures.length > 0 && (
|
||||
<div className="de-management-sync-failures" aria-label="同步失败明细">
|
||||
{managementSyncFailures.map((failure) => (
|
||||
<div key={failure.id} className="de-management-sync-failure" title={failure.error || failure.id}>
|
||||
<button
|
||||
key={failure.id}
|
||||
type="button"
|
||||
className="de-management-sync-failure"
|
||||
title={failure.error || failure.id}
|
||||
onClick={() => setSelectedSyncFailure(failure)}
|
||||
>
|
||||
<span>{syncFailureTime(failure)}</span>
|
||||
<strong>{entityTypeLabel(failure.entityType)}</strong>
|
||||
<em>{failure.id}</em>
|
||||
<small>{syncFailureSummary(failure)}</small>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -1790,6 +1805,52 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedSyncFailure && (
|
||||
<div className="de-task-manager-modal-overlay" role="presentation" onClick={() => setSelectedSyncFailure(null)}>
|
||||
<div
|
||||
className="de-event-detail-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="同步失败详情"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="de-task-manager-modal-head">
|
||||
<div>
|
||||
<span className="de-drawer-eyebrow">SYNC FAILURE</span>
|
||||
<h3>同步失败详情</h3>
|
||||
<p>{entityTypeLabel(selectedSyncFailure.entityType)} / {selectedSyncFailure.operation}</p>
|
||||
</div>
|
||||
<button type="button" className="de-schedule-close" onClick={() => setSelectedSyncFailure(null)}>×</button>
|
||||
</div>
|
||||
<div className="de-event-detail-grid">
|
||||
<div><span>Outbox</span><strong>{selectedSyncFailure.id}</strong></div>
|
||||
<div><span>实体</span><strong>{selectedSyncFailure.entityId}</strong></div>
|
||||
<div><span>类型</span><strong>{entityTypeLabel(selectedSyncFailure.entityType)}</strong></div>
|
||||
<div><span>操作</span><strong>{selectedSyncFailure.operation}</strong></div>
|
||||
<div><span>重试</span><strong>{selectedSyncFailure.attempts} 次</strong></div>
|
||||
<div><span>上次尝试</span><strong>{syncFailureTime(selectedSyncFailure)}</strong></div>
|
||||
<div><span>下次重试</span><strong>{syncFailureRetryTime(selectedSyncFailure)}</strong></div>
|
||||
<div><span>状态</span><strong>{selectedSyncFailure.dueForRetry ? '等待补偿' : '退避中'}</strong></div>
|
||||
</div>
|
||||
<div className="de-event-detail-result">
|
||||
<span>失败原因</span>
|
||||
<p>{selectedSyncFailure.error || '暂无错误详情。'}</p>
|
||||
</div>
|
||||
<div className="de-event-modal-actions">
|
||||
<button type="button" className="de-btn-secondary de-btn-sm" onClick={() => setSelectedSyncFailure(null)}>关闭</button>
|
||||
<button
|
||||
type="button"
|
||||
className="de-btn-primary de-btn-sm"
|
||||
disabled={actionBusy === 'flush_management_sync'}
|
||||
onClick={() => { void flushManagementSync().then(() => setSelectedSyncFailure(null)); }}
|
||||
>
|
||||
{actionBusy === 'flush_management_sync' ? '同步中...' : '立即重试'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedEvent && (
|
||||
<div className="de-task-manager-modal-overlay" role="presentation" onClick={closeEventDetail}>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user