@iwonz/ywtc 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +15 -7
- package/dist/adapters/confluence.d.ts +23 -2
- package/dist/adapters/confluence.js +64 -9
- package/dist/adapters/confluence.js.map +1 -1
- package/dist/adapters/yandex.d.ts +16 -2
- package/dist/adapters/yandex.js +49 -4
- package/dist/adapters/yandex.js.map +1 -1
- package/dist/cli-arguments.d.ts +3 -1
- package/dist/cli-arguments.js +6 -2
- package/dist/cli-arguments.js.map +1 -1
- package/dist/cli.js +31 -19
- package/dist/cli.js.map +1 -1
- package/dist/clients/confluence-cloud.d.ts +1 -0
- package/dist/clients/confluence-cloud.js +7 -0
- package/dist/clients/confluence-cloud.js.map +1 -1
- package/dist/clients/confluence.d.ts +2 -0
- package/dist/clients/confluence.js +21 -0
- package/dist/clients/confluence.js.map +1 -1
- package/dist/clients/yandex-wiki.d.ts +3 -1
- package/dist/clients/yandex-wiki.js +23 -5
- package/dist/clients/yandex-wiki.js.map +1 -1
- package/dist/content-model.js +3 -2
- package/dist/content-model.js.map +1 -1
- package/dist/doctor.d.ts +7 -2
- package/dist/doctor.js +8 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain.d.ts +48 -2
- package/dist/domain.js +5 -1
- package/dist/domain.js.map +1 -1
- package/dist/graph.d.ts +2 -1
- package/dist/graph.js +105 -15
- package/dist/graph.js.map +1 -1
- package/dist/migrator.d.ts +4 -14
- package/dist/migrator.js +349 -68
- package/dist/migrator.js.map +1 -1
- package/dist/report-catalog.d.ts +47 -2
- package/dist/report-catalog.js +71 -9
- package/dist/report-catalog.js.map +1 -1
- package/dist/report.d.ts +96 -24
- package/dist/report.js +303 -36
- package/dist/report.js.map +1 -1
- package/dist/state.d.ts +8 -2
- package/dist/state.js +25 -1
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
package/dist/report.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import type { GraphEdgeStatus, ProviderKind, TransferScope } from './domain.js';
|
|
1
|
+
import type { DiscoveredPage, GraphEdgeStatus, ProviderKind, SourceResource, SourceSelection, SourceStatus, TransferScope } from './domain.js';
|
|
2
2
|
import { type IssueInput, type MigrationIssue } from './issues.js';
|
|
3
|
-
import type { RunStatus, StateLoadSource } from './state.js';
|
|
3
|
+
import type { PageTransferStatus, ResourceTransferState, RunStatus, StateLoadSource } from './state.js';
|
|
4
|
+
export type ReportInventoryStatus = 'pending' | 'complete' | 'failed';
|
|
5
|
+
export type ReportResourceStatus = ResourceTransferState['status'];
|
|
6
|
+
export interface TransferReportResource {
|
|
7
|
+
sourceId: string;
|
|
8
|
+
sourceName: string;
|
|
9
|
+
kind: Exclude<SourceResource['kind'], 'grid'>;
|
|
10
|
+
status: ReportResourceStatus;
|
|
11
|
+
sourceBytes?: number;
|
|
12
|
+
transferredBytes?: number;
|
|
13
|
+
destinationId?: string;
|
|
14
|
+
destinationName?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
4
17
|
export interface TransferReportPage {
|
|
5
18
|
sourceCanonicalId: string;
|
|
6
19
|
sourceId: string;
|
|
@@ -11,13 +24,17 @@ export interface TransferReportPage {
|
|
|
11
24
|
destinationUrl?: string;
|
|
12
25
|
parentId?: string;
|
|
13
26
|
depth: number;
|
|
14
|
-
status:
|
|
27
|
+
status: PageTransferStatus;
|
|
28
|
+
inventoryStatus: ReportInventoryStatus;
|
|
15
29
|
resources: number;
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
resourceItems: TransferReportResource[];
|
|
31
|
+
sourceBytes?: number;
|
|
32
|
+
transferredBytes: number;
|
|
33
|
+
bytesUnknown: number;
|
|
18
34
|
links: number;
|
|
19
35
|
images: number;
|
|
20
36
|
tables: number;
|
|
37
|
+
grids: number;
|
|
21
38
|
codeBlocks: number;
|
|
22
39
|
degraded: boolean;
|
|
23
40
|
resourceFallbacks: Array<{
|
|
@@ -35,6 +52,58 @@ export interface TransferReportEdge {
|
|
|
35
52
|
status: GraphEdgeStatus;
|
|
36
53
|
message?: string;
|
|
37
54
|
}
|
|
55
|
+
export interface TransferReportIssue extends MigrationIssue {
|
|
56
|
+
pageTitle?: string;
|
|
57
|
+
pageAnchor?: string;
|
|
58
|
+
resourceName?: string;
|
|
59
|
+
action: string;
|
|
60
|
+
}
|
|
61
|
+
type PageStatusCounts = Record<PageTransferStatus, number>;
|
|
62
|
+
type ResourceStatusCounts = Record<ReportResourceStatus, number>;
|
|
63
|
+
type EdgeStatusCounts = Record<GraphEdgeStatus, number>;
|
|
64
|
+
export interface TransferReportSummary {
|
|
65
|
+
pages: number;
|
|
66
|
+
returnedDescendants: number;
|
|
67
|
+
selectedDescendants: number;
|
|
68
|
+
excludedPages: number;
|
|
69
|
+
maximumDepth: number;
|
|
70
|
+
hierarchyGaps: number;
|
|
71
|
+
hierarchyRepairs: number;
|
|
72
|
+
complete: number;
|
|
73
|
+
partial: number;
|
|
74
|
+
failed: number;
|
|
75
|
+
blocked: number;
|
|
76
|
+
conflicts: number;
|
|
77
|
+
unprocessedPages: number;
|
|
78
|
+
resources: number;
|
|
79
|
+
attachments: number;
|
|
80
|
+
resourceImages: number;
|
|
81
|
+
resourcesComplete: number;
|
|
82
|
+
resourcesPartial: number;
|
|
83
|
+
resourcesFailed: number;
|
|
84
|
+
resourcesPlanned: number;
|
|
85
|
+
bytes: number;
|
|
86
|
+
sourceBytes: number;
|
|
87
|
+
bytesUnknown: number;
|
|
88
|
+
links: number;
|
|
89
|
+
images: number;
|
|
90
|
+
tables: number;
|
|
91
|
+
grids: number;
|
|
92
|
+
codeBlocks: number;
|
|
93
|
+
cycles: number;
|
|
94
|
+
normalizations: number;
|
|
95
|
+
degradations: number;
|
|
96
|
+
warnings: number;
|
|
97
|
+
errors: number;
|
|
98
|
+
retryableFailures: number;
|
|
99
|
+
inventoryComplete: boolean;
|
|
100
|
+
inventoriedPages: number;
|
|
101
|
+
inventoryFailedPages: number;
|
|
102
|
+
durationMs?: number;
|
|
103
|
+
pageStatuses: PageStatusCounts;
|
|
104
|
+
resourceStatuses: ResourceStatusCounts;
|
|
105
|
+
traversalStatuses: EdgeStatusCounts;
|
|
106
|
+
}
|
|
38
107
|
export interface TransferReportData {
|
|
39
108
|
runId: string;
|
|
40
109
|
startedAt: string;
|
|
@@ -42,6 +111,8 @@ export interface TransferReportData {
|
|
|
42
111
|
status: RunStatus;
|
|
43
112
|
dryRun: boolean;
|
|
44
113
|
scope: TransferScope;
|
|
114
|
+
sourceStatus: SourceStatus;
|
|
115
|
+
selection: SourceSelection;
|
|
45
116
|
direction: string;
|
|
46
117
|
source: {
|
|
47
118
|
provider: ProviderKind;
|
|
@@ -66,31 +137,26 @@ export interface TransferReportData {
|
|
|
66
137
|
};
|
|
67
138
|
pages: TransferReportPage[];
|
|
68
139
|
edges: TransferReportEdge[];
|
|
140
|
+
hierarchyRepairs: Array<{
|
|
141
|
+
sourceCanonicalId: string;
|
|
142
|
+
mode: 'moved' | 'recreated';
|
|
143
|
+
oldParentId?: string;
|
|
144
|
+
newParentId: string;
|
|
145
|
+
oldDestinationId: string;
|
|
146
|
+
newDestinationId: string;
|
|
147
|
+
verified: boolean;
|
|
148
|
+
}>;
|
|
69
149
|
warnings: string[];
|
|
70
150
|
errors: string[];
|
|
71
|
-
issues:
|
|
151
|
+
issues: TransferReportIssue[];
|
|
72
152
|
outputs: Array<{
|
|
73
153
|
label: string;
|
|
74
154
|
value: string;
|
|
75
155
|
}>;
|
|
76
|
-
summary:
|
|
77
|
-
pages: number;
|
|
78
|
-
complete: number;
|
|
79
|
-
partial: number;
|
|
80
|
-
failed: number;
|
|
81
|
-
resources: number;
|
|
82
|
-
bytes: number;
|
|
83
|
-
bytesUnknown: number;
|
|
84
|
-
links: number;
|
|
85
|
-
cycles: number;
|
|
86
|
-
normalizations: number;
|
|
87
|
-
degradations: number;
|
|
88
|
-
warnings: number;
|
|
89
|
-
errors: number;
|
|
90
|
-
retryableFailures: number;
|
|
91
|
-
};
|
|
156
|
+
summary: TransferReportSummary;
|
|
92
157
|
}
|
|
93
158
|
export interface MigrationReportPaths {
|
|
159
|
+
runId: string;
|
|
94
160
|
directory: string;
|
|
95
161
|
html: string;
|
|
96
162
|
json: string;
|
|
@@ -103,9 +169,15 @@ export declare class TransferReport {
|
|
|
103
169
|
readonly data: TransferReportData;
|
|
104
170
|
private readonly previews;
|
|
105
171
|
private readonly issueCollector;
|
|
106
|
-
constructor(input: Omit<TransferReportData, 'runId' | 'startedAt' | 'status' | 'pages' | 'edges' | 'warnings' | 'errors' | 'issues' | 'outputs' | 'summary'>
|
|
107
|
-
|
|
172
|
+
constructor(input: Omit<TransferReportData, 'runId' | 'startedAt' | 'status' | 'sourceStatus' | 'selection' | 'pages' | 'edges' | 'hierarchyRepairs' | 'warnings' | 'errors' | 'issues' | 'outputs' | 'summary'> & {
|
|
173
|
+
sourceStatus?: SourceStatus;
|
|
174
|
+
});
|
|
175
|
+
registerPages(pages: DiscoveredPage[]): void;
|
|
176
|
+
updatePage(sourceCanonicalId: string, update: Partial<TransferReportPage>): void;
|
|
177
|
+
addPage(page: TransferReportPage, previews?: PreviewPair, collectIssues?: boolean): void;
|
|
108
178
|
setEdges(edges: TransferReportEdge[]): void;
|
|
179
|
+
setSelection(selection: SourceSelection): void;
|
|
180
|
+
addHierarchyRepair(repair: TransferReportData['hierarchyRepairs'][number]): void;
|
|
109
181
|
issue(input: IssueInput): void;
|
|
110
182
|
warning(message: string): void;
|
|
111
183
|
error(message: string): void;
|
package/dist/report.js
CHANGED
|
@@ -1,7 +1,66 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { IssueCollector } from './issues.js';
|
|
5
|
+
const pageStatuses = [
|
|
6
|
+
'discovered',
|
|
7
|
+
'planned',
|
|
8
|
+
'skeleton-created',
|
|
9
|
+
'complete',
|
|
10
|
+
'partial',
|
|
11
|
+
'conflict',
|
|
12
|
+
'failed',
|
|
13
|
+
'blocked',
|
|
14
|
+
];
|
|
15
|
+
const resourceStatuses = ['planned', 'complete', 'partial', 'failed'];
|
|
16
|
+
const edgeStatuses = ['resolved', 'external', 'inaccessible', 'broken', 'cycle', 'unsupported'];
|
|
17
|
+
function countRecord(values) {
|
|
18
|
+
return Object.fromEntries(values.map((value) => [value, 0]));
|
|
19
|
+
}
|
|
20
|
+
function emptySummary() {
|
|
21
|
+
return {
|
|
22
|
+
pages: 0,
|
|
23
|
+
returnedDescendants: 0,
|
|
24
|
+
selectedDescendants: 0,
|
|
25
|
+
excludedPages: 0,
|
|
26
|
+
maximumDepth: 0,
|
|
27
|
+
hierarchyGaps: 0,
|
|
28
|
+
hierarchyRepairs: 0,
|
|
29
|
+
complete: 0,
|
|
30
|
+
partial: 0,
|
|
31
|
+
failed: 0,
|
|
32
|
+
blocked: 0,
|
|
33
|
+
conflicts: 0,
|
|
34
|
+
unprocessedPages: 0,
|
|
35
|
+
resources: 0,
|
|
36
|
+
attachments: 0,
|
|
37
|
+
resourceImages: 0,
|
|
38
|
+
resourcesComplete: 0,
|
|
39
|
+
resourcesPartial: 0,
|
|
40
|
+
resourcesFailed: 0,
|
|
41
|
+
resourcesPlanned: 0,
|
|
42
|
+
bytes: 0,
|
|
43
|
+
sourceBytes: 0,
|
|
44
|
+
bytesUnknown: 0,
|
|
45
|
+
links: 0,
|
|
46
|
+
images: 0,
|
|
47
|
+
tables: 0,
|
|
48
|
+
grids: 0,
|
|
49
|
+
codeBlocks: 0,
|
|
50
|
+
cycles: 0,
|
|
51
|
+
normalizations: 0,
|
|
52
|
+
degradations: 0,
|
|
53
|
+
warnings: 0,
|
|
54
|
+
errors: 0,
|
|
55
|
+
retryableFailures: 0,
|
|
56
|
+
inventoryComplete: false,
|
|
57
|
+
inventoriedPages: 0,
|
|
58
|
+
inventoryFailedPages: 0,
|
|
59
|
+
pageStatuses: countRecord(pageStatuses),
|
|
60
|
+
resourceStatuses: countRecord(resourceStatuses),
|
|
61
|
+
traversalStatuses: countRecord(edgeStatuses),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
5
64
|
function escapeHtml(value) {
|
|
6
65
|
return String(value ?? '')
|
|
7
66
|
.replaceAll('&', '&')
|
|
@@ -22,74 +81,229 @@ function safeHttpUrl(value) {
|
|
|
22
81
|
}
|
|
23
82
|
}
|
|
24
83
|
function statusClass(value) {
|
|
25
|
-
return
|
|
84
|
+
return value === 'complete' || value === 'completed' ? 'ok'
|
|
85
|
+
: /partial|warning|conflict|planned|discovered|skeleton|interrupted|cancelled/.test(value) ? 'warn'
|
|
86
|
+
: /failed|blocked|error/.test(value) ? 'bad'
|
|
87
|
+
: 'muted';
|
|
88
|
+
}
|
|
89
|
+
function pageAnchor(sourceCanonicalId) {
|
|
90
|
+
return `page-${createHash('sha256').update(sourceCanonicalId).digest('hex').slice(0, 12)}`;
|
|
91
|
+
}
|
|
92
|
+
function formatBytes(value) {
|
|
93
|
+
if (value < 1024)
|
|
94
|
+
return `${value} B`;
|
|
95
|
+
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
96
|
+
let amount = value;
|
|
97
|
+
let unit = -1;
|
|
98
|
+
do {
|
|
99
|
+
amount /= 1024;
|
|
100
|
+
unit += 1;
|
|
101
|
+
} while (amount >= 1024 && unit < units.length - 1);
|
|
102
|
+
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${units[unit]}`;
|
|
103
|
+
}
|
|
104
|
+
function formatDuration(durationMs) {
|
|
105
|
+
if (durationMs === undefined)
|
|
106
|
+
return 'in progress';
|
|
107
|
+
if (durationMs < 1000)
|
|
108
|
+
return `${durationMs} ms`;
|
|
109
|
+
const seconds = Math.round(durationMs / 1000);
|
|
110
|
+
if (seconds < 60)
|
|
111
|
+
return `${seconds} s`;
|
|
112
|
+
const minutes = Math.floor(seconds / 60);
|
|
113
|
+
return `${minutes} min ${seconds % 60} s`;
|
|
114
|
+
}
|
|
115
|
+
function issueAction(issue) {
|
|
116
|
+
if (issue.retryable)
|
|
117
|
+
return 'Repeat the same migration to retry this object.';
|
|
118
|
+
if (issue.severity === 'degradation')
|
|
119
|
+
return 'Data remains represented; compare the source and destination previews.';
|
|
120
|
+
if (issue.severity === 'error')
|
|
121
|
+
return 'Review the reason and affected object before resuming the migration.';
|
|
122
|
+
if (issue.severity === 'normalization')
|
|
123
|
+
return 'No action is required.';
|
|
124
|
+
return 'Review the affected object and the page details below.';
|
|
125
|
+
}
|
|
126
|
+
function statCard(label, value, detail) {
|
|
127
|
+
return `<div class="stat"><small>${escapeHtml(label)}</small><strong>${escapeHtml(value)}</strong>${detail ? `<span>${escapeHtml(detail)}</span>` : ''}</div>`;
|
|
128
|
+
}
|
|
129
|
+
function statusCells(counts, statuses) {
|
|
130
|
+
return statuses.map((status) => `<td><span class="status ${statusClass(status)}">${escapeHtml(status)}</span><strong>${escapeHtml(counts[status] ?? 0)}</strong></td>`).join('');
|
|
26
131
|
}
|
|
27
132
|
function renderReport(data, previews) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
133
|
+
const sourceUrl = safeHttpUrl(data.source.url);
|
|
134
|
+
const destinationUrl = safeHttpUrl(data.destination.url);
|
|
135
|
+
const inventoryState = data.summary.inventoryComplete
|
|
136
|
+
? `${data.summary.inventoriedPages}/${data.summary.pages} pages inspected`
|
|
137
|
+
: `${data.summary.inventoriedPages}/${data.summary.pages} pages inspected · ${data.summary.inventoryFailedPages} failed`;
|
|
138
|
+
const sourceCards = [
|
|
139
|
+
statCard('Selected pages', data.summary.pages, 'accessible in selected scope'),
|
|
140
|
+
statCard('Returned descendants', data.summary.returnedDescendants, `${data.summary.excludedPages} excluded by status`),
|
|
141
|
+
statCard('Tree depth', data.summary.maximumDepth, `${data.summary.hierarchyGaps} hierarchy gaps · ${data.summary.hierarchyRepairs} repaired`),
|
|
142
|
+
statCard('Files', data.summary.resources, `${data.summary.attachments} attachments · ${data.summary.resourceImages} images`),
|
|
143
|
+
statCard('Source bytes', formatBytes(data.summary.sourceBytes), data.summary.bytesUnknown ? `${data.summary.bytesUnknown} sizes unknown` : 'all sizes known'),
|
|
144
|
+
statCard('Transferred bytes', formatBytes(data.summary.bytes), 'confirmed complete files'),
|
|
145
|
+
statCard('Links', data.summary.links, `${data.summary.images} embedded images`),
|
|
146
|
+
statCard('Structured content', data.summary.tables + data.summary.codeBlocks, `${data.summary.tables} tables · ${data.summary.grids} grids · ${data.summary.codeBlocks} code blocks`),
|
|
147
|
+
].join('');
|
|
148
|
+
const excludedRows = data.selection.excluded.map((page) => `<tr><td>${escapeHtml(page.title ?? page.locator)}</td><td><code>${escapeHtml(page.id)}</code></td><td>${escapeHtml(page.status ?? 'non-current')}</td><td>${escapeHtml(page.reason)}</td></tr>`).join('');
|
|
149
|
+
const gapRows = data.selection.hierarchyGaps.map((gap) => `<tr><td><code>${escapeHtml(gap.pageId)}</code></td><td><code>${escapeHtml(gap.missingParentId ?? 'unknown')}</code></td><td><code>${escapeHtml(gap.selectedParentId)}</code></td><td>${escapeHtml(gap.reason)}</td></tr>`).join('');
|
|
150
|
+
const repairRows = data.hierarchyRepairs.map((repair) => `<tr><td><code>${escapeHtml(repair.sourceCanonicalId)}</code></td><td>${escapeHtml(repair.mode)}</td><td><code>${escapeHtml(repair.oldParentId ?? 'none')}</code></td><td><code>${escapeHtml(repair.newParentId)}</code></td><td><code>${escapeHtml(repair.oldDestinationId)} → ${escapeHtml(repair.newDestinationId)}</code></td><td>${repair.verified ? 'verified' : 'planned'}</td></tr>`).join('');
|
|
151
|
+
const selectionDetails = excludedRows || gapRows || repairRows || data.selection.notes.length
|
|
152
|
+
? `<section class="panel" aria-labelledby="selection-title"><div class="section-head"><div><h2 id="selection-title">Selection and hierarchy</h2><p>Every source-status exclusion, projected hierarchy gap, and destination repair recorded for this run.</p></div></div>${excludedRows ? `<div class="table-wrap"><h3>Excluded pages</h3><table><thead><tr><th>Page</th><th>Source ID</th><th>Status</th><th>Reason</th></tr></thead><tbody>${excludedRows}</tbody></table></div>` : ''}${gapRows ? `<div class="table-wrap"><h3>Hierarchy gaps</h3><table><thead><tr><th>Page</th><th>Missing parent</th><th>Projected parent</th><th>Reason</th></tr></thead><tbody>${gapRows}</tbody></table></div>` : ''}${repairRows ? `<div class="table-wrap"><h3>Destination repairs</h3><table><thead><tr><th>Source page</th><th>Mode</th><th>Old parent</th><th>New parent</th><th>Destination mapping</th><th>Outcome</th></tr></thead><tbody>${repairRows}</tbody></table></div>` : ''}${data.selection.notes.length ? `<div class="callout"><h3>Provider notes</h3><ul>${data.selection.notes.map((note) => `<li>${escapeHtml(note)}</li>`).join('')}</ul></div>` : ''}</section>`
|
|
153
|
+
: '';
|
|
154
|
+
const pageOutcomeStatuses = ['complete', 'partial', 'failed', 'blocked', 'conflict', 'planned', 'discovered', 'skeleton-created'];
|
|
155
|
+
const resourceOutcomeStatuses = ['complete', 'partial', 'failed', 'planned'];
|
|
156
|
+
const attentionIssues = data.issues.filter((issue) => issue.severity !== 'normalization');
|
|
157
|
+
const normalizationIssues = data.issues.filter((issue) => issue.severity === 'normalization');
|
|
158
|
+
const attentionSummary = [
|
|
159
|
+
`${data.summary.errors} errors`,
|
|
160
|
+
`${data.summary.warnings} warnings`,
|
|
161
|
+
`${data.summary.degradations} degradations`,
|
|
162
|
+
`${data.summary.retryableFailures} retryable`,
|
|
163
|
+
].map((item) => `<span>${escapeHtml(item)}</span>`).join('');
|
|
164
|
+
const issueRow = (issue) => {
|
|
165
|
+
const target = issue.pageAnchor ? `<a href="#${escapeHtml(issue.pageAnchor)}">${escapeHtml(issue.pageTitle ?? issue.pageId)}</a>` : '<span>run-level</span>';
|
|
166
|
+
return `<li class="issue ${escapeHtml(issue.severity)}"><div><span class="status ${statusClass(issue.severity)}">${escapeHtml(issue.severity)}</span><strong>${escapeHtml(issue.code)}</strong><span>${escapeHtml(issue.phase)}</span>${issue.retryable ? '<span class="retry">retryable</span>' : ''}${issue.occurrences > 1 ? `<span>${issue.occurrences} occurrences</span>` : ''}</div><p>${escapeHtml(issue.message)}</p><p class="issue-context">Affected: ${target}${issue.resourceName ? ` · resource <strong>${escapeHtml(issue.resourceName)}</strong>` : ''}</p><p class="action">${escapeHtml(issue.action)}</p></li>`;
|
|
167
|
+
};
|
|
168
|
+
const attention = `<section class="panel attention" aria-labelledby="attention-title"><div class="section-head"><div><h2 id="attention-title">Needs attention</h2><p>Problems and fidelity changes that may require review or a rerun.</p></div><span class="count ${attentionIssues.length ? 'warn' : 'ok'}">${attentionIssues.length}</span></div><div class="mini-stats">${attentionSummary}</div>${attentionIssues.length ? `<ul class="issue-list">${attentionIssues.map(issueRow).join('')}</ul>` : '<p class="empty ok">No errors, warnings, conflicts, or degradations were recorded.</p>'}</section>`;
|
|
34
169
|
const pageRows = data.pages.map((page) => {
|
|
35
170
|
const source = safeHttpUrl(page.sourceUrl);
|
|
36
171
|
const destination = safeHttpUrl(page.destinationUrl);
|
|
37
172
|
const preview = previews.get(page.sourceCanonicalId);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
173
|
+
const pageIssues = data.issues.filter((issue) => issue.pageId === page.sourceCanonicalId);
|
|
174
|
+
const resourceRows = page.resourceItems.map((resource) => `<tr><td><strong>${escapeHtml(resource.sourceName)}</strong><small>${escapeHtml(resource.kind)}</small></td><td><span class="status ${statusClass(resource.status)}">${escapeHtml(resource.status)}</span></td><td>${resource.sourceBytes === undefined ? 'unknown' : escapeHtml(formatBytes(resource.sourceBytes))}</td><td>${resource.transferredBytes === undefined ? '—' : escapeHtml(formatBytes(resource.transferredBytes))}</td><td>${escapeHtml(resource.destinationName ?? '—')}</td><td>${resource.error ? `<span class="bad">${escapeHtml(resource.error)}</span>` : '—'}</td></tr>`).join('');
|
|
175
|
+
const metrics = `${page.resources} files · ${page.links} links · ${page.images} images · ${page.tables} tables · ${page.codeBlocks} code blocks`;
|
|
176
|
+
return `<details id="${escapeHtml(pageAnchor(page.sourceCanonicalId))}" class="page" style="--depth:${Math.min(page.depth, 8)}"${page.status === 'complete' ? '' : ' open'}><summary><span class="status ${statusClass(page.status)}">${escapeHtml(page.status)}</span><strong>${escapeHtml(page.sourceTitle)}</strong><span>${escapeHtml(metrics)}</span></summary>
|
|
177
|
+
<div class="meta"><span>inventory: <strong>${escapeHtml(page.inventoryStatus)}</strong></span><span>depth ${page.depth}</span>${page.parentId ? `<span>parent ${escapeHtml(page.parentId)}</span>` : ''}${source ? `<a href="${escapeHtml(source)}" target="_blank" rel="noreferrer">source</a>` : ''}${destination ? `<a href="${escapeHtml(destination)}" target="_blank" rel="noreferrer">destination</a>` : ''}<span>${escapeHtml(page.sourceCanonicalId)}</span><span>source ${page.sourceBytes === undefined ? 'size unknown' : escapeHtml(formatBytes(page.sourceBytes))} · transferred ${escapeHtml(formatBytes(page.transferredBytes))}${page.bytesUnknown ? ` · ${page.bytesUnknown} unknown` : ''}</span></div>
|
|
178
|
+
${page.error ? `<div class="callout bad"><strong>Page error</strong><p>${escapeHtml(page.error)}</p></div>` : ''}
|
|
179
|
+
${pageIssues.length ? `<div><h3>Page issues</h3><ul class="issue-list compact">${pageIssues.map(issueRow).join('')}</ul></div>` : ''}
|
|
180
|
+
${page.resourceItems.length ? `<div class="table-wrap"><h3>Resources</h3><table><thead><tr><th>Source resource</th><th>Outcome</th><th>Source size</th><th>Transferred</th><th>Destination name</th><th>Problem</th></tr></thead><tbody>${resourceRows}</tbody></table></div>` : '<p class="muted">No transferable resources on this page.</p>'}
|
|
181
|
+
${preview ? `<div class="compare"><section><h3>Source preview</h3><p class="muted">Normalized source content</p><iframe title="Source preview: ${escapeHtml(page.sourceTitle)}" sandbox srcdoc="${escapeHtml(preview.source)}"></iframe></section><section><h3>Destination preview</h3><p class="muted">Body read back from destination</p><iframe title="Destination preview: ${escapeHtml(page.sourceTitle)}" sandbox srcdoc="${escapeHtml(preview.destination)}"></iframe></section></div>` : '<p class="muted">Destination preview is unavailable because no body was read back.</p>'}
|
|
182
|
+
${page.resourceFallbacks.length ? `<div class="callout warn"><h3>Resource fallbacks</h3><ul>${page.resourceFallbacks.map((item) => `<li><strong>${escapeHtml(item.name)}</strong> — ${escapeHtml(item.error)}</li>`).join('')}</ul></div>` : ''}
|
|
42
183
|
${(page.normalizations ?? []).length ? `<div><h3>Safe normalizations</h3><ul>${(page.normalizations ?? []).map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div>` : ''}
|
|
43
|
-
|
|
184
|
+
</details>`;
|
|
44
185
|
}).join('');
|
|
45
|
-
const edgeRows = data.edges.map((edge) =>
|
|
186
|
+
const edgeRows = data.edges.map((edge) => {
|
|
187
|
+
const from = data.pages.find((page) => page.sourceCanonicalId === edge.from)?.sourceTitle ?? edge.from;
|
|
188
|
+
const to = edge.to ? data.pages.find((page) => page.sourceCanonicalId === edge.to)?.sourceTitle ?? edge.to : '—';
|
|
189
|
+
return `<tr><td><span class="status ${statusClass(edge.status)}">${escapeHtml(edge.status)}</span></td><td>${escapeHtml(from)}</td><td>${escapeHtml(to)}</td><td><code>${escapeHtml(edge.raw)}</code></td><td>${escapeHtml(edge.message ?? '—')}</td></tr>`;
|
|
190
|
+
}).join('');
|
|
191
|
+
const traversalSummary = edgeStatuses.filter((status) => data.summary.traversalStatuses[status] > 0).map((status) => `<span><strong>${data.summary.traversalStatuses[status]}</strong> ${escapeHtml(status)}</span>`).join('');
|
|
192
|
+
const traversalBody = data.edges.length
|
|
193
|
+
? `<div class="mini-stats">${traversalSummary}</div><div class="table-wrap"><table><thead><tr><th>Status</th><th>From</th><th>To</th><th>Reference</th><th>Reason</th></tr></thead><tbody>${edgeRows}</tbody></table></div>`
|
|
194
|
+
: data.scope === 'linked'
|
|
195
|
+
? '<p class="empty">No traversal edges were discovered in linked scope.</p>'
|
|
196
|
+
: `<p class="empty">Traversal edges are collected only for <code>linked</code> scope. This ${escapeHtml(data.scope)} run still inventoried ${data.summary.links} content links and verified them as part of each page body.</p>`;
|
|
46
197
|
const outputRows = data.outputs.map((output) => `<li><strong>${escapeHtml(output.label)}</strong> <code>${escapeHtml(output.value)}</code></li>`).join('');
|
|
47
|
-
const
|
|
48
|
-
? `<section class="panel"><
|
|
198
|
+
const normalizations = normalizationIssues.length
|
|
199
|
+
? `<section class="panel"><div class="section-head"><div><h2>Safe normalizations</h2><p>Expected provider changes that did not lose meaningful data.</p></div><span class="count">${normalizationIssues.length}</span></div><ul class="issue-list compact">${normalizationIssues.map(issueRow).join('')}</ul></section>`
|
|
49
200
|
: '';
|
|
50
|
-
const normalizations = issueSection('Safe normalizations', data.issues.filter((issue) => issue.severity === 'normalization'));
|
|
51
|
-
const degradations = issueSection('Degradations', data.issues.filter((issue) => issue.severity === 'degradation'), 'warnings');
|
|
52
|
-
const retryable = issueSection('Retryable failures', data.issues.filter((issue) => issue.retryable), 'warnings');
|
|
53
|
-
const finalErrors = issueSection('Final errors', data.issues.filter((issue) => issue.severity === 'error'), 'error');
|
|
54
201
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ywtc transfer ${escapeHtml(data.runId)}</title><style>
|
|
55
|
-
:root{font:15px/1.5 system-ui,sans-serif;color:#
|
|
56
|
-
</style></head><body><main><header class="head"><div><h1>ywtc transfer report</h1><p>${escapeHtml(data.direction)} · ${escapeHtml(data.scope)} · ${data.dryRun ? 'dry-run' : 'live'}</p></div><div><span class="status ${statusClass(data.status)}">${escapeHtml(data.status)}</span><p class="muted">${escapeHtml(data.runId)}</p></div></header
|
|
202
|
+
:root{font:15px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f4f6fa;color-scheme:light}*{box-sizing:border-box}body{margin:0}a{color:#175cd3}main{max-width:1360px;margin:auto;padding:32px}.head,.section-head{display:flex;justify-content:space-between;gap:20px;align-items:flex-start}.head h1{margin:0}.head p,.section-head p{margin:.25rem 0}.muted{color:#667085}.run-meta,.meta,.mini-stats{display:flex;flex-wrap:wrap;gap:10px 18px}.run-meta{margin-top:18px;padding:14px 0;border-top:1px solid #dfe4ed}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin:18px 0}.stat,.page,.panel{background:#fff;border:1px solid #dfe4ed;border-radius:12px;box-shadow:0 1px 2px rgba(16,24,40,.04)}.stat{padding:14px}.stat small,.stat strong,.stat span{display:block}.stat strong{font-size:24px}.stat span{font-size:12px;color:#667085;margin-top:2px}.panel{padding:20px;margin-top:20px;overflow:hidden}.panel h2{margin:0}.count{display:grid;place-items:center;min-width:38px;height:38px;border-radius:20px;background:#eef2f8;font-weight:800}.page{margin:10px 0 10px calc(var(--depth) * 12px);padding:14px;scroll-margin-top:18px}.page:target{outline:3px solid #84adff}.page summary{display:grid;grid-template-columns:120px minmax(180px,1fr) auto;gap:12px;cursor:pointer;align-items:center}.status{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.03em}.ok{color:#087443}.warn{color:#9a6500}.bad{color:#b42318}.meta{margin:14px 0;color:#667085;font-size:13px}.compare{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px}.compare section{min-width:0}.compare h3{margin-bottom:0}.compare iframe{width:100%;height:420px;border:1px solid #dfe4ed;border-radius:8px;background:#fff}.table-wrap{overflow:auto;margin-top:14px}table{border-collapse:collapse;width:100%;min-width:680px}th,td{padding:9px;border-bottom:1px solid #e6e9ef;text-align:left;vertical-align:top}th{font-size:12px;text-transform:uppercase;color:#667085}td strong,td small{display:block}.outcomes td strong{font-size:18px;margin-top:3px}.issue-list{list-style:none;padding:0;margin:14px 0 0}.issue{border-left:4px solid #f79009;background:#fffaeb;padding:12px 14px;margin:8px 0;border-radius:6px}.issue.error{border-color:#f04438;background:#fef3f2}.issue.degradation{border-color:#f79009}.issue.normalization{border-color:#98a2b3;background:#f9fafb}.issue>div{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.issue p{margin:.35rem 0}.issue-context,.action{font-size:13px}.retry{font-size:11px;background:#fff0c2;border-radius:12px;padding:2px 7px}.compact .issue{padding:9px 11px}.callout{padding:12px 14px;border-radius:8px;background:#f9fafb;margin:12px 0}.callout.warn{background:#fffaeb}.callout.bad{background:#fef3f2}.empty{padding:14px;border-radius:8px;background:#f9fafb}.attention{border-top:4px solid #f79009}.attention:has(.empty.ok){border-top-color:#12b76a}.context-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}.context-grid dl{margin:0}.context-grid dt{font-size:12px;text-transform:uppercase;color:#667085}.context-grid dd{margin:2px 0 12px;overflow-wrap:anywhere}code{overflow-wrap:anywhere}@media(max-width:760px){main{padding:16px}.page{margin-left:0}.page summary{grid-template-columns:1fr}.compare{grid-template-columns:1fr}.head,.section-head{display:block}.count{margin-top:10px}.stats{grid-template-columns:1fr 1fr}.panel{padding:14px}}@media(max-width:420px){.stats{grid-template-columns:1fr}}
|
|
203
|
+
</style></head><body><main><header class="head"><div><h1>ywtc transfer report</h1><p>${escapeHtml(data.direction)} · ${escapeHtml(data.scope)} · source status ${escapeHtml(data.sourceStatus)} · ${data.dryRun ? 'dry-run' : 'live transfer'}</p></div><div><span class="status ${statusClass(data.status)}">${escapeHtml(data.status)}</span><p class="muted">${escapeHtml(data.runId)}</p></div></header>
|
|
204
|
+
<div class="run-meta"><span>Started <strong>${escapeHtml(data.startedAt)}</strong></span>${data.finishedAt ? `<span>Finished <strong>${escapeHtml(data.finishedAt)}</strong></span>` : ''}<span>Duration <strong>${escapeHtml(formatDuration(data.summary.durationMs))}</strong></span><span>Inventory <strong class="${data.summary.inventoryComplete ? 'ok' : 'warn'}">${escapeHtml(inventoryState)}</strong></span></div>
|
|
205
|
+
<section class="panel" aria-labelledby="source-title"><div class="section-head"><div><h2 id="source-title">Source scope inventory</h2><p>Root plus ${escapeHtml(data.summary.returnedDescendants)} accessible descendants; ${escapeHtml(data.summary.selectedDescendants)} selected and ${escapeHtml(data.summary.excludedPages)} excluded with source status <code>${escapeHtml(data.sourceStatus)}</code>.</p></div><span class="status ${data.summary.inventoryComplete ? 'ok' : 'warn'}">${data.summary.inventoryComplete ? 'complete' : 'incomplete'}</span></div><div class="stats">${sourceCards}</div></section>
|
|
206
|
+
${selectionDetails}
|
|
207
|
+
<section class="panel" aria-labelledby="outcome-title"><div class="section-head"><div><h2 id="outcome-title">Transfer outcomes</h2><p>Every selected page and discovered file is counted in exactly one status.</p></div><span class="count">${data.summary.pages}</span></div><div class="table-wrap"><table class="outcomes"><thead><tr><th>Object</th>${pageOutcomeStatuses.map((status) => `<th>${escapeHtml(status)}</th>`).join('')}<th>Total</th></tr></thead><tbody><tr><th>Pages</th>${statusCells(data.summary.pageStatuses, pageOutcomeStatuses)}<td><strong>${data.summary.pages}</strong></td></tr></tbody></table></div><div class="table-wrap"><table class="outcomes"><thead><tr><th>Object</th>${resourceOutcomeStatuses.map((status) => `<th>${escapeHtml(status)}</th>`).join('')}<th>Total</th></tr></thead><tbody><tr><th>Files</th>${statusCells(data.summary.resourceStatuses, resourceOutcomeStatuses)}<td><strong>${data.summary.resources}</strong></td></tr></tbody></table></div></section>
|
|
208
|
+
${attention}
|
|
209
|
+
<section aria-labelledby="pages-title"><div class="section-head"><div><h2 id="pages-title">Pages</h2><p>Projected hierarchy with exact outcomes, source metrics, resources, issues, and verified previews.</p></div></div>${pageRows || '<p class="empty">No source page was resolved before the run stopped.</p>'}</section>
|
|
210
|
+
<section class="panel" aria-labelledby="links-title"><div class="section-head"><div><h2 id="links-title">Link traversal</h2><p>Traversal evidence used to select pages in linked scope; content link totals are shown in source inventory.</p></div><span class="count">${data.edges.length}</span></div>${traversalBody}</section>
|
|
211
|
+
${normalizations}
|
|
212
|
+
<section class="panel" aria-labelledby="context-title"><h2 id="context-title">Run context and outputs</h2><div class="context-grid"><dl><dt>Source</dt><dd>${sourceUrl ? `<a href="${escapeHtml(sourceUrl)}">${escapeHtml(data.source.title)}</a>` : escapeHtml(data.source.title)}<br>${escapeHtml(data.source.provider)} · profile ${escapeHtml(data.source.profile)} · root ${escapeHtml(data.source.rootId)}</dd><dt>Destination</dt><dd>${destinationUrl ? `<a href="${escapeHtml(destinationUrl)}">${escapeHtml(data.destination.provider)}</a>` : escapeHtml(data.destination.provider)}${data.destination.profile ? ` · profile ${escapeHtml(data.destination.profile)}` : ''}<br>${escapeHtml(data.destination.id)}</dd></dl><dl><dt>State</dt><dd>${escapeHtml(data.state.id)}<br><code>${escapeHtml(data.state.path)}</code><br>${escapeHtml(data.state.mode)} · ${escapeHtml(data.state.source)}</dd>${data.state.backupPath ? `<dt>State backup</dt><dd><code>${escapeHtml(data.state.backupPath)}</code></dd>` : ''}<dt>Outputs</dt><dd>${outputRows ? `<ul>${outputRows}</ul>` : 'No result output was created.'}</dd></dl></div><p><a class="download" download="report.json" href="report.json">Download report JSON</a></p></section>
|
|
213
|
+
</main></body></html>`;
|
|
57
214
|
}
|
|
58
215
|
export class TransferReport {
|
|
59
216
|
data;
|
|
60
217
|
previews = new Map();
|
|
61
218
|
issueCollector = new IssueCollector();
|
|
62
219
|
constructor(input) {
|
|
220
|
+
const sourceStatus = input.sourceStatus ?? 'all';
|
|
63
221
|
this.data = {
|
|
64
222
|
runId: randomUUID(),
|
|
65
223
|
startedAt: new Date().toISOString(),
|
|
66
224
|
status: 'running',
|
|
225
|
+
sourceStatus,
|
|
226
|
+
selection: {
|
|
227
|
+
requestedStatus: sourceStatus,
|
|
228
|
+
effectiveStatus: sourceStatus,
|
|
229
|
+
returnedDescendants: 0,
|
|
230
|
+
selectedDescendants: 0,
|
|
231
|
+
excluded: [],
|
|
232
|
+
maximumDepth: 0,
|
|
233
|
+
hierarchyGaps: [],
|
|
234
|
+
notes: [],
|
|
235
|
+
},
|
|
67
236
|
pages: [],
|
|
68
237
|
edges: [],
|
|
238
|
+
hierarchyRepairs: [],
|
|
69
239
|
warnings: [],
|
|
70
240
|
errors: [],
|
|
71
241
|
issues: [],
|
|
72
242
|
outputs: [],
|
|
73
|
-
summary:
|
|
243
|
+
summary: emptySummary(),
|
|
74
244
|
...input,
|
|
75
245
|
};
|
|
76
246
|
}
|
|
77
|
-
|
|
247
|
+
registerPages(pages) {
|
|
248
|
+
for (const page of pages) {
|
|
249
|
+
if (this.data.pages.some((item) => item.sourceCanonicalId === page.canonicalId))
|
|
250
|
+
continue;
|
|
251
|
+
this.data.pages.push({
|
|
252
|
+
sourceCanonicalId: page.canonicalId,
|
|
253
|
+
sourceId: page.id,
|
|
254
|
+
sourceTitle: page.title,
|
|
255
|
+
sourceUrl: page.url,
|
|
256
|
+
...(page.projectedParentId ? { parentId: page.projectedParentId } : {}),
|
|
257
|
+
depth: page.depth,
|
|
258
|
+
status: 'discovered',
|
|
259
|
+
inventoryStatus: 'pending',
|
|
260
|
+
resources: 0,
|
|
261
|
+
resourceItems: [],
|
|
262
|
+
transferredBytes: 0,
|
|
263
|
+
bytesUnknown: 0,
|
|
264
|
+
links: 0,
|
|
265
|
+
images: 0,
|
|
266
|
+
tables: 0,
|
|
267
|
+
grids: 0,
|
|
268
|
+
codeBlocks: 0,
|
|
269
|
+
degraded: false,
|
|
270
|
+
resourceFallbacks: [],
|
|
271
|
+
warnings: [],
|
|
272
|
+
normalizations: [],
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
updatePage(sourceCanonicalId, update) {
|
|
277
|
+
const index = this.data.pages.findIndex((page) => page.sourceCanonicalId === sourceCanonicalId);
|
|
278
|
+
if (index < 0)
|
|
279
|
+
throw new Error(`Report page ${sourceCanonicalId} is not registered`);
|
|
280
|
+
this.data.pages[index] = { ...this.data.pages[index], ...update };
|
|
281
|
+
}
|
|
282
|
+
addPage(page, previews, collectIssues = true) {
|
|
78
283
|
page.warnings = [...new Set(page.warnings)];
|
|
79
284
|
page.normalizations = [...new Set(page.normalizations ?? [])];
|
|
80
285
|
const index = this.data.pages.findIndex((item) => item.sourceCanonicalId === page.sourceCanonicalId);
|
|
81
286
|
if (index >= 0)
|
|
82
|
-
this.data.pages[index] = page;
|
|
287
|
+
this.data.pages[index] = { ...this.data.pages[index], ...page };
|
|
83
288
|
else
|
|
84
289
|
this.data.pages.push(page);
|
|
85
290
|
if (previews)
|
|
86
291
|
this.previews.set(page.sourceCanonicalId, previews);
|
|
292
|
+
if (!collectIssues)
|
|
293
|
+
return;
|
|
87
294
|
for (const message of page.normalizations)
|
|
88
295
|
this.issue({ code: `normalization:${message}`, severity: 'normalization', phase: 'page', pageId: page.sourceCanonicalId, retryable: false, message });
|
|
89
296
|
for (const message of page.warnings)
|
|
90
297
|
this.issue({ code: `warning:${message}`, severity: page.degraded ? 'degradation' : 'warning', phase: 'page', pageId: page.sourceCanonicalId, retryable: false, message });
|
|
91
298
|
}
|
|
92
299
|
setEdges(edges) { this.data.edges = edges; }
|
|
300
|
+
setSelection(selection) {
|
|
301
|
+
this.data.selection = selection;
|
|
302
|
+
this.data.sourceStatus = selection.effectiveStatus;
|
|
303
|
+
}
|
|
304
|
+
addHierarchyRepair(repair) {
|
|
305
|
+
this.data.hierarchyRepairs.push(repair);
|
|
306
|
+
}
|
|
93
307
|
issue(input) { this.issueCollector.add(input); }
|
|
94
308
|
warning(message) { this.issue({ code: `warning:${message}`, severity: 'warning', phase: 'report', retryable: false, message }); }
|
|
95
309
|
error(message) { this.issue({ code: `error:${message}`, severity: 'error', phase: 'report', retryable: false, message }); }
|
|
@@ -97,24 +311,77 @@ export class TransferReport {
|
|
|
97
311
|
finish(status) {
|
|
98
312
|
this.data.status = status;
|
|
99
313
|
this.data.finishedAt = new Date().toISOString();
|
|
100
|
-
|
|
101
|
-
this.data.
|
|
314
|
+
const pageById = new Map(this.data.pages.map((page) => [page.sourceCanonicalId, page]));
|
|
315
|
+
this.data.issues = this.issueCollector.list().map((issue) => {
|
|
316
|
+
const page = issue.pageId ? pageById.get(issue.pageId) : undefined;
|
|
317
|
+
const resource = issue.resourceId ? page?.resourceItems.find((item) => item.sourceId === issue.resourceId) : undefined;
|
|
318
|
+
return {
|
|
319
|
+
...issue,
|
|
320
|
+
...(page ? { pageTitle: page.sourceTitle, pageAnchor: pageAnchor(page.sourceCanonicalId) } : {}),
|
|
321
|
+
...(resource ? { resourceName: resource.sourceName } : {}),
|
|
322
|
+
action: issueAction(issue),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
this.data.warnings = this.data.issues.filter((issue) => issue.severity === 'warning').map((issue) => issue.message);
|
|
102
326
|
this.data.errors = this.data.issues.filter((issue) => issue.severity === 'error').map((issue) => issue.message);
|
|
327
|
+
const pageStatusCounts = countRecord(pageStatuses);
|
|
328
|
+
const resourceStatusCounts = countRecord(resourceStatuses);
|
|
329
|
+
const traversalStatusCounts = countRecord(edgeStatuses);
|
|
330
|
+
for (const page of this.data.pages)
|
|
331
|
+
pageStatusCounts[page.status] += 1;
|
|
332
|
+
const resources = this.data.pages.flatMap((page) => page.resourceItems);
|
|
333
|
+
for (const resource of resources)
|
|
334
|
+
resourceStatusCounts[resource.status] += 1;
|
|
335
|
+
for (const edge of this.data.edges)
|
|
336
|
+
traversalStatusCounts[edge.status] += 1;
|
|
337
|
+
const started = Date.parse(this.data.startedAt);
|
|
338
|
+
const finished = Date.parse(this.data.finishedAt);
|
|
339
|
+
const sourceBytes = resources.reduce((total, resource) => total + (resource.sourceBytes ?? 0), 0);
|
|
340
|
+
const transferredBytes = resources.reduce((total, resource) => total + (resource.status === 'complete' ? resource.transferredBytes ?? 0 : 0), 0);
|
|
341
|
+
const inventoriedPages = this.data.pages.filter((page) => page.inventoryStatus === 'complete').length;
|
|
342
|
+
const inventoryFailedPages = this.data.pages.filter((page) => page.inventoryStatus === 'failed').length;
|
|
103
343
|
this.data.summary = {
|
|
104
344
|
pages: this.data.pages.length,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
345
|
+
returnedDescendants: this.data.selection.returnedDescendants,
|
|
346
|
+
selectedDescendants: this.data.selection.selectedDescendants,
|
|
347
|
+
excludedPages: this.data.selection.excluded.length,
|
|
348
|
+
maximumDepth: this.data.selection.maximumDepth,
|
|
349
|
+
hierarchyGaps: this.data.selection.hierarchyGaps.length,
|
|
350
|
+
hierarchyRepairs: this.data.hierarchyRepairs.length,
|
|
351
|
+
complete: pageStatusCounts.complete,
|
|
352
|
+
partial: pageStatusCounts.partial,
|
|
353
|
+
failed: pageStatusCounts.failed,
|
|
354
|
+
blocked: pageStatusCounts.blocked,
|
|
355
|
+
conflicts: pageStatusCounts.conflict,
|
|
356
|
+
unprocessedPages: pageStatusCounts.discovered + pageStatusCounts.planned + pageStatusCounts['skeleton-created'],
|
|
357
|
+
resources: resources.length,
|
|
358
|
+
attachments: resources.filter((resource) => resource.kind === 'attachment').length,
|
|
359
|
+
resourceImages: resources.filter((resource) => resource.kind === 'image').length,
|
|
360
|
+
resourcesComplete: resourceStatusCounts.complete,
|
|
361
|
+
resourcesPartial: resourceStatusCounts.partial,
|
|
362
|
+
resourcesFailed: resourceStatusCounts.failed,
|
|
363
|
+
resourcesPlanned: resourceStatusCounts.planned,
|
|
364
|
+
bytes: transferredBytes,
|
|
365
|
+
sourceBytes,
|
|
366
|
+
bytesUnknown: resources.filter((resource) => resource.sourceBytes === undefined).length,
|
|
111
367
|
links: this.data.pages.reduce((total, page) => total + page.links, 0),
|
|
112
|
-
|
|
368
|
+
images: this.data.pages.reduce((total, page) => total + page.images, 0),
|
|
369
|
+
tables: this.data.pages.reduce((total, page) => total + page.tables, 0),
|
|
370
|
+
grids: this.data.pages.reduce((total, page) => total + page.grids, 0),
|
|
371
|
+
codeBlocks: this.data.pages.reduce((total, page) => total + page.codeBlocks, 0),
|
|
372
|
+
cycles: traversalStatusCounts.cycle,
|
|
113
373
|
normalizations: this.issueCollector.count('normalization'),
|
|
114
374
|
degradations: this.issueCollector.count('degradation'),
|
|
115
|
-
warnings: this.issueCollector.count('warning')
|
|
375
|
+
warnings: this.issueCollector.count('warning'),
|
|
116
376
|
errors: this.issueCollector.count('error'),
|
|
117
377
|
retryableFailures: this.issueCollector.countRetryableFailures(),
|
|
378
|
+
inventoryComplete: this.data.pages.length > 0 && inventoriedPages === this.data.pages.length,
|
|
379
|
+
inventoriedPages,
|
|
380
|
+
inventoryFailedPages,
|
|
381
|
+
...(Number.isFinite(started) && Number.isFinite(finished) ? { durationMs: Math.max(0, finished - started) } : {}),
|
|
382
|
+
pageStatuses: pageStatusCounts,
|
|
383
|
+
resourceStatuses: resourceStatusCounts,
|
|
384
|
+
traversalStatuses: traversalStatusCounts,
|
|
118
385
|
};
|
|
119
386
|
}
|
|
120
387
|
async write(reportsDirectory) {
|
|
@@ -128,7 +395,7 @@ export class TransferReport {
|
|
|
128
395
|
await writeFile(htmlTemporary, renderReport(this.data, this.previews), { mode: 0o600 });
|
|
129
396
|
await rename(jsonTemporary, json);
|
|
130
397
|
await rename(htmlTemporary, html);
|
|
131
|
-
return { directory, html, json };
|
|
398
|
+
return { runId: this.data.runId, directory, html, json };
|
|
132
399
|
}
|
|
133
400
|
}
|
|
134
401
|
//# sourceMappingURL=report.js.map
|