@jrpool/kilotest 38.0.0 → 39.0.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/.markdownlint-cli2.jsonc +12 -0
- package/.vscode/settings.json +3 -0
- package/CLAUDE.md +5 -0
- package/README.md +6 -4
- package/ai0BalanceForm/index.html +1 -1
- package/alerts.js +3 -1
- package/api/getReport.js +66 -0
- package/api/listDiagnoses.js +157 -0
- package/api/listIssues.js +225 -0
- package/api/listReports.js +87 -0
- package/api/listViolators.js +164 -0
- package/api/requestFeature.js +56 -0
- package/api/requestRetest.js +102 -0
- package/api/requestTest.js +106 -0
- package/api/routes.js +97 -0
- package/api/schemas.js +297 -0
- package/api/testSuite.js +294 -0
- package/api/util.js +149 -0
- package/diagnoses/index.js +2 -2
- package/docs/{AI-TOOL.md → AI-TOOLS.md} +54 -26
- package/docs/SERVICE.md +4 -1
- package/docs/decisions/001-api-design.md +59 -49
- package/eslint.config.mjs +13 -3
- package/index.html +1 -1
- package/index.js +201 -188
- package/issues/index.js +13 -21
- package/jsconfig.json +8 -0
- package/llms-full.txt +2 -2
- package/llms.txt +16 -11
- package/mcp.js +129 -65
- package/openapi.yaml +1234 -572
- package/package.json +18 -11
- package/reannotate/index.js +26 -15
- package/reannotateForm/index.js +15 -13
- package/redocly.yaml +4 -0
- package/reportHideForm/index.html +1 -1
- package/reportHideForm/index.js +24 -38
- package/reportIssue/index.js +3 -3
- package/reportIssues/index.js +128 -1
- package/reportUnhideForm/index.js +36 -44
- package/reportsExpungeForm/index.js +22 -16
- package/reportsPruneForm/index.js +22 -12
- package/reportsRewindForm/index.js +17 -9
- package/retestRec/index.js +8 -4
- package/retestRecForm/index.js +18 -6
- package/scripts/generate-openapi.js +83 -0
- package/targets/index.js +18 -13
- package/testOrder/index.js +11 -8
- package/tutorial/index.html +12 -12
- package/util.js +580 -539
- package/reportIssue/api.js +0 -183
- package/reportIssues/util.js +0 -138
- package/reportList/api.js +0 -83
- package/reportSummary/api.js +0 -181
- package/researchAgent.js +0 -206
- package/target/api.js +0 -126
- package/testRecForm/api.js +0 -55
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jrpool/kilotest",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "39.0.0",
|
|
4
|
+
"description": "Ensemble testing for front-end web quality",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node index",
|
|
8
|
-
"lint": "npx eslint",
|
|
9
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
"lint": "npx eslint && npx markdownlint-cli2",
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
10
|
+
"test:api": "node api/testSuite",
|
|
11
|
+
"generate:openapi": "node scripts/generate-openapi"
|
|
10
12
|
},
|
|
11
13
|
"repository": {
|
|
12
14
|
"type": "git",
|
|
@@ -20,7 +22,8 @@
|
|
|
20
22
|
"author": "Jonathan Robert Pool <pool@jpdev.pro>",
|
|
21
23
|
"license": "MIT",
|
|
22
24
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/jrpool/kilotest/issues"
|
|
25
|
+
"url": "https://github.com/jrpool/kilotest/issues",
|
|
26
|
+
"email": "info@kilotest.com"
|
|
24
27
|
},
|
|
25
28
|
"homepage": "https://github.com/jrpool/kilotest",
|
|
26
29
|
"dependencies": {
|
|
@@ -30,12 +33,16 @@
|
|
|
30
33
|
"zod": "*"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
|
-
"@eslint/css": "
|
|
34
|
-
"@eslint/js": "
|
|
35
|
-
"@eslint/json": "
|
|
36
|
-
"@eslint/markdown": "
|
|
37
|
-
"
|
|
38
|
-
"
|
|
36
|
+
"@eslint/css": "*",
|
|
37
|
+
"@eslint/js": "*",
|
|
38
|
+
"@eslint/json": "*",
|
|
39
|
+
"@eslint/markdown": "*",
|
|
40
|
+
"@redocly/cli": "*",
|
|
41
|
+
"eslint": "*",
|
|
42
|
+
"globals": "*",
|
|
43
|
+
"js-yaml": "*",
|
|
44
|
+
"markdownlint-cli2": "*",
|
|
45
|
+
"zod-openapi": "*"
|
|
39
46
|
},
|
|
40
47
|
"allowScripts": {
|
|
41
48
|
"fsevents@2.3.2": true
|
package/reannotate/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {annotateReport,
|
|
8
|
+
const {annotateReport, getReportExtracts, ruleIDs} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -15,22 +15,33 @@ const path = require('path');
|
|
|
15
15
|
exports.answer = async authCode => {
|
|
16
16
|
// If the authorization code is valid:
|
|
17
17
|
if (authCode === process.env.AUTH_CODE) {
|
|
18
|
-
// Get
|
|
19
|
-
const
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
// Get data on the available reports.
|
|
19
|
+
const reportExtracts = await getReportExtracts();
|
|
20
|
+
// If any exist:
|
|
21
|
+
if (reportExtracts.length) {
|
|
22
|
+
// For each report:
|
|
23
|
+
for (const reportExtract of reportExtracts) {
|
|
24
|
+
const {timeStamp, jobID} = reportExtract;
|
|
25
|
+
// Reannotate it.
|
|
26
|
+
const annotationError = await annotateReport(ruleIDs, timeStamp, jobID);
|
|
27
|
+
// If this failed:
|
|
28
|
+
if (annotationError) {
|
|
29
|
+
// Return an error page.
|
|
30
|
+
return {
|
|
31
|
+
status: 'error',
|
|
32
|
+
message: annotationError
|
|
33
|
+
};
|
|
34
|
+
}
|
|
32
35
|
}
|
|
33
36
|
}
|
|
37
|
+
// Otherwis, i.e. if it failed:
|
|
38
|
+
else {
|
|
39
|
+
// Return an error page.
|
|
40
|
+
return {
|
|
41
|
+
status: 'error',
|
|
42
|
+
message: 'Got data on no available reports'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
34
45
|
// If every annotation succeeded, get the answer page.
|
|
35
46
|
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
36
47
|
// Return it.
|
package/reannotateForm/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/*
|
|
2
2
|
index.js
|
|
3
|
-
Discloses unclassified rules.
|
|
3
|
+
Discloses unclassified and reclassified rules and serves a form to reannotate reports.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {getIssue, getReport,
|
|
8
|
+
const {getIssue, getReport, getReportExtracts, ruleIDs} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -13,15 +13,17 @@ const path = require('path');
|
|
|
13
13
|
|
|
14
14
|
// Adds parameters to a query for the answer page.
|
|
15
15
|
const populateQuery = async query => {
|
|
16
|
-
|
|
17
|
-
const stillUnclassified = {};
|
|
16
|
+
// Initialize data on rule classification.
|
|
18
17
|
const reClassified = {};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const {
|
|
18
|
+
const stillUnclassified = {};
|
|
19
|
+
// Get an extract of all available reports.
|
|
20
|
+
const reportExtracts = await getReportExtracts();
|
|
21
|
+
// For each report:
|
|
22
|
+
for (const reportExtract of reportExtracts) {
|
|
23
|
+
const {jobID, timeStamp} = reportExtract;
|
|
24
|
+
// Get it.
|
|
25
|
+
const report = await getReport(timeStamp, jobID);
|
|
26
|
+
const {acts = [], error, id} = report;
|
|
25
27
|
// If this failed:
|
|
26
28
|
if (error) {
|
|
27
29
|
// Populate the query with the reason.
|
|
@@ -44,14 +46,14 @@ const populateQuery = async query => {
|
|
|
44
46
|
// Add the rule and the report to the rules with changed issue IDs.
|
|
45
47
|
reClassified[which] ??= {};
|
|
46
48
|
reClassified[which][ruleID] ??= new Set();
|
|
47
|
-
reClassified[which][ruleID].add(
|
|
49
|
+
reClassified[which][ruleID].add(id);
|
|
48
50
|
}
|
|
49
51
|
// Otherwise, if the instance and the rule both have no issue ID:
|
|
50
|
-
else if (!
|
|
52
|
+
else if (!issueID){
|
|
51
53
|
// Add the rule and the report to the rules that are still unclassified.
|
|
52
54
|
stillUnclassified[which] ??= {};
|
|
53
55
|
stillUnclassified[which][ruleID] ??= new Set();
|
|
54
|
-
stillUnclassified[which][ruleID].add(
|
|
56
|
+
stillUnclassified[which][ruleID].add(id);
|
|
55
57
|
}
|
|
56
58
|
});
|
|
57
59
|
}
|
package/redocly.yaml
ADDED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
__reports__
|
|
21
21
|
</fieldset>
|
|
22
22
|
<p><label>
|
|
23
|
-
Authorization code: <input size="3"
|
|
23
|
+
Authorization code: <input size="3" minlength="3" maxlength="3" name="authCode" required>
|
|
24
24
|
</label></p>
|
|
25
25
|
<p><button type="submit">Submit</button></p>
|
|
26
26
|
</form>
|
package/reportHideForm/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const {getReportExtracts, hiddenReportsPath, makeReportsExtract, reportsPath} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -20,20 +20,21 @@ exports.answer = async (_, search) => {
|
|
|
20
20
|
if (jobName) {
|
|
21
21
|
// If the authorization code is valid:
|
|
22
22
|
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const fileName = `${jobName}.json`;
|
|
24
|
+
try {
|
|
25
|
+
// Move the specified report to the directory of hidden reports.
|
|
26
|
+
await fs.rename(path.join(reportsPath, fileName), path.join(hiddenReportsPath, fileName));
|
|
27
|
+
// Update the data on the available reports.
|
|
28
|
+
await makeReportsExtract();
|
|
29
|
+
}
|
|
25
30
|
// If this failed:
|
|
26
|
-
|
|
31
|
+
catch (error) {
|
|
27
32
|
// Return why.
|
|
28
33
|
return {
|
|
29
34
|
status: 'error',
|
|
30
|
-
message:
|
|
31
|
-
}
|
|
35
|
+
message: `Hiding report ${jobName} failed (${error.message})`
|
|
36
|
+
};
|
|
32
37
|
}
|
|
33
|
-
// Otherwise, i.e. if it succeeded, add a hiddenness property to the log.
|
|
34
|
-
log.hidden = true;
|
|
35
|
-
// Save the updated log.
|
|
36
|
-
await fs.writeFile(path.join(logsPath, `${jobName}.json`), getJSON(log));
|
|
37
38
|
}
|
|
38
39
|
// Otherwise, i.e. if the authorization code is invalid:
|
|
39
40
|
else {
|
|
@@ -44,33 +45,21 @@ exports.answer = async (_, search) => {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
|
-
const reportFileNames = await fs.readdir(reportsPath);
|
|
48
48
|
// Initialize an array of report specifications.
|
|
49
49
|
const reportSpecs = [];
|
|
50
|
+
// Get data on all available reports.
|
|
51
|
+
const reportExtracts = await getReportExtracts();
|
|
50
52
|
// For each report:
|
|
51
|
-
for (const
|
|
52
|
-
const
|
|
53
|
-
// Get its log.
|
|
54
|
-
const log = await getLog(timeStamp, jobID);
|
|
55
|
-
// If this failed:
|
|
56
|
-
if (log.error) {
|
|
57
|
-
// Return why.
|
|
58
|
-
return {
|
|
59
|
-
status: 'error',
|
|
60
|
-
message: log.error
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Otherwise, i.e. if it succeeded:
|
|
64
|
-
const {what, hidden} = log;
|
|
53
|
+
for (const reportExtract of reportExtracts) {
|
|
54
|
+
const {jobID, timeStamp, what} = reportExtract;
|
|
65
55
|
// Add the report to the array.
|
|
66
56
|
reportSpecs.push({
|
|
67
57
|
what,
|
|
68
58
|
timeStamp,
|
|
69
|
-
jobID
|
|
70
|
-
hidden
|
|
59
|
+
jobID
|
|
71
60
|
});
|
|
72
61
|
}
|
|
73
|
-
// Sort the
|
|
62
|
+
// Sort the data by page name and then by time stamp.
|
|
74
63
|
reportSpecs.sort((a, b) => {
|
|
75
64
|
if (a.what === b.what) {
|
|
76
65
|
return a.timeStamp.localeCompare(b.timeStamp);
|
|
@@ -79,17 +68,14 @@ exports.answer = async (_, search) => {
|
|
|
79
68
|
});
|
|
80
69
|
const lines = [];
|
|
81
70
|
const margin = ' '.repeat(12);
|
|
82
|
-
// For each report:
|
|
71
|
+
// For each available report:
|
|
83
72
|
reportSpecs.forEach(spec => {
|
|
84
|
-
const {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
|
|
91
|
-
);
|
|
92
|
-
}
|
|
73
|
+
const {jobID, timeStamp, what} = spec;
|
|
74
|
+
const specString = `${what} (job <code>${jobID}</code> at ${timeStamp})`;
|
|
75
|
+
// Add a line with a radio button to hide it.
|
|
76
|
+
lines.push(
|
|
77
|
+
`${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
|
|
78
|
+
);
|
|
93
79
|
});
|
|
94
80
|
const query = {
|
|
95
81
|
reports: lines.join('\n'),
|
package/reportIssue/index.js
CHANGED
|
@@ -37,7 +37,7 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
37
37
|
// Otherwise, i.e. if it succeeded, add the issue summary to the query.
|
|
38
38
|
query.issue = issues[issueID]?.summary;
|
|
39
39
|
// If adding the issue summary failed:
|
|
40
|
-
if (!
|
|
40
|
+
if (!query.issue) {
|
|
41
41
|
// Populate the query with the reason.
|
|
42
42
|
query.error = 'Issue not found';
|
|
43
43
|
// Stop populating the query.
|
|
@@ -112,7 +112,7 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
112
112
|
// Convert the violator data to an array.
|
|
113
113
|
violators = Object.entries(violators).map(entry => ({
|
|
114
114
|
catalogIndex: entry[0],
|
|
115
|
-
...
|
|
115
|
+
...entry[1]
|
|
116
116
|
}));
|
|
117
117
|
// Sort the violators in XPath order.
|
|
118
118
|
violators.sort((a, b) => a.pathID.localeCompare(b.pathID));
|
|
@@ -133,7 +133,7 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
133
133
|
if (tagName) {
|
|
134
134
|
lines.push(`${margin} <li>Tag name: <code>${tagName}</code></li>`);
|
|
135
135
|
}
|
|
136
|
-
if (text && !
|
|
136
|
+
if (text && !['HTML', 'HEAD', 'BODY', 'MAIN', 'NOSCRIPT'].includes(tagName)) {
|
|
137
137
|
const textString = text.split('\n').join(' … ');
|
|
138
138
|
lines.push(`${margin} <li>Text: <q>${htmlSafe(textString)}</q></li>`);
|
|
139
139
|
}
|
package/reportIssues/index.js
CHANGED
|
@@ -5,20 +5,147 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {getData} = require('./util');
|
|
9
8
|
const {
|
|
9
|
+
getPageData,
|
|
10
10
|
getPageDataStrings,
|
|
11
|
+
getReport,
|
|
12
|
+
getToolList,
|
|
11
13
|
getWCAGLink,
|
|
12
14
|
getWeightName,
|
|
13
15
|
htmlSafe,
|
|
14
16
|
isHidden,
|
|
17
|
+
isValidReport,
|
|
18
|
+
objectSort,
|
|
15
19
|
tools
|
|
16
20
|
} = require('../util');
|
|
21
|
+
const issuesClassification = require('testilo/procs/score/tic').issues;
|
|
17
22
|
const fs = require('fs/promises');
|
|
18
23
|
const path = require('path');
|
|
19
24
|
|
|
20
25
|
// FUNCTIONS
|
|
21
26
|
|
|
27
|
+
// Returns data on the issues reported by a report.
|
|
28
|
+
const getIssuesData = async (timeStamp, jobID) => {
|
|
29
|
+
// Get the report.
|
|
30
|
+
const report = await getReport(timeStamp, jobID);
|
|
31
|
+
const reportIsHidden = await isHidden(timeStamp, jobID);
|
|
32
|
+
// If it exists and is hidden:
|
|
33
|
+
if (reportIsHidden) {
|
|
34
|
+
return {error: 'Report is not available'}
|
|
35
|
+
}
|
|
36
|
+
// Otherwise, if it exists and is valid:
|
|
37
|
+
if (isValidReport(report)) {
|
|
38
|
+
// Initialize the temporary data.
|
|
39
|
+
const temp = {
|
|
40
|
+
issues: {},
|
|
41
|
+
reporters: new Set(),
|
|
42
|
+
violators: new Set()
|
|
43
|
+
};
|
|
44
|
+
// Initialize the final data.
|
|
45
|
+
const final = {
|
|
46
|
+
reporters: [],
|
|
47
|
+
reporterList: '',
|
|
48
|
+
reporterCount: 0,
|
|
49
|
+
violatorCount: 0,
|
|
50
|
+
preventions: report.jobData.preventions,
|
|
51
|
+
issues: {
|
|
52
|
+
4: [],
|
|
53
|
+
3: [],
|
|
54
|
+
2: [],
|
|
55
|
+
1: []
|
|
56
|
+
},
|
|
57
|
+
issueCount: 0
|
|
58
|
+
};
|
|
59
|
+
// For each act in the report:
|
|
60
|
+
report.acts.forEach(act => {
|
|
61
|
+
// If it is a test act:
|
|
62
|
+
if (act.type === 'test') {
|
|
63
|
+
const {result, which} = act;
|
|
64
|
+
const instances = result?.standardResult?.instances ?? [];
|
|
65
|
+
// For each of its standard instances:
|
|
66
|
+
instances.forEach(instance => {
|
|
67
|
+
const {catalogIndex, issueID} = instance;
|
|
68
|
+
// If the instance identifies its rule as belonging to a non-ignorable issue:
|
|
69
|
+
if (issueID && issueID !== 'ignorable') {
|
|
70
|
+
const issueClassification = issuesClassification[issueID];
|
|
71
|
+
// If the issue has a current weighted classification:
|
|
72
|
+
if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
|
|
73
|
+
const {summary, wcag, weight, why} = issueClassification;
|
|
74
|
+
// Initialize the temporary data on the issue if necessary.
|
|
75
|
+
temp.issues[issueID] ??= {
|
|
76
|
+
issueID,
|
|
77
|
+
summary,
|
|
78
|
+
wcag,
|
|
79
|
+
why,
|
|
80
|
+
weight,
|
|
81
|
+
reporters: new Set(),
|
|
82
|
+
reporterList: '',
|
|
83
|
+
violators: new Set()
|
|
84
|
+
};
|
|
85
|
+
// Ensure the tool is in the temporary data.
|
|
86
|
+
temp.issues[issueID].reporters.add(which);
|
|
87
|
+
temp.reporters.add(which);
|
|
88
|
+
// If the instance has a catalog index:
|
|
89
|
+
if (catalogIndex) {
|
|
90
|
+
// Ensure the violator is in the temporary data.
|
|
91
|
+
temp.issues[issueID].violators.add(catalogIndex);
|
|
92
|
+
temp.violators.add(catalogIndex);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// Finish populating the final data.
|
|
100
|
+
final.reporterList = getToolList(temp.reporters);
|
|
101
|
+
final.reporterCount = temp.reporters.size;
|
|
102
|
+
final.violatorCount = temp.violators.size;
|
|
103
|
+
Object.values(temp.issues).forEach(issue => {
|
|
104
|
+
const {issueID, summary, wcag, why, weight} = issue;
|
|
105
|
+
const finalIssue = {
|
|
106
|
+
issueID,
|
|
107
|
+
summary,
|
|
108
|
+
wcag,
|
|
109
|
+
why,
|
|
110
|
+
weight
|
|
111
|
+
};
|
|
112
|
+
finalIssue.reporterList = getToolList(issue.reporters);
|
|
113
|
+
finalIssue.reporterCount = issue.reporters.size;
|
|
114
|
+
finalIssue.violatorCount = issue.violators.size;
|
|
115
|
+
final.issues[issue.weight].push(finalIssue);
|
|
116
|
+
});
|
|
117
|
+
final.issueCount = Object.keys(temp.issues).length;
|
|
118
|
+
// For each weight:
|
|
119
|
+
[4, 3, 2, 1].forEach(weight => {
|
|
120
|
+
// Sort its issues in the final data alphabetically by reporter names.
|
|
121
|
+
objectSort(final.issues[weight], 'reporterList', 'alpha');
|
|
122
|
+
// Sort the issues again in descending reporter-count order, making this the primary order.
|
|
123
|
+
objectSort(final.issues[weight], 'reporterCount', 'numericDown');
|
|
124
|
+
});
|
|
125
|
+
// Return the data.
|
|
126
|
+
return final;
|
|
127
|
+
}
|
|
128
|
+
// Otherwise, i.e. if it is invalid or does not exist, return this.
|
|
129
|
+
return {error: 'Report missing or invalid.'};
|
|
130
|
+
};
|
|
131
|
+
// Get page and issues data from a report.
|
|
132
|
+
const getData = async (timeStamp, jobID) => {
|
|
133
|
+
const pageData = await getPageData(timeStamp, jobID);
|
|
134
|
+
const issuesData = await getIssuesData(timeStamp, jobID);
|
|
135
|
+
const pageError = pageData.error || '';
|
|
136
|
+
const issuesError = issuesData.error || '';
|
|
137
|
+
const errors = [pageError, issuesError].filter(Boolean).join('; ');
|
|
138
|
+
// If the data of either type are missing or invalid:
|
|
139
|
+
if (errors) {
|
|
140
|
+
// Return this.
|
|
141
|
+
return {error: errors};
|
|
142
|
+
}
|
|
143
|
+
// Otherwise, return the data.
|
|
144
|
+
return {
|
|
145
|
+
pageData,
|
|
146
|
+
issuesData
|
|
147
|
+
};
|
|
148
|
+
};
|
|
22
149
|
// Adds parameters to a query for the answer page.
|
|
23
150
|
const populateQuery = async (timeStamp, jobID, query) => {
|
|
24
151
|
// Get data on the target and its issues according to the report.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const {hiddenReportsPath, makeReportsExtract, reportsPath} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -20,20 +20,20 @@ exports.answer = async (_, search) => {
|
|
|
20
20
|
if (jobName) {
|
|
21
21
|
// If the authorization code is valid:
|
|
22
22
|
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const fileName = `${jobName}.json`;
|
|
24
|
+
try {
|
|
25
|
+
// Move the report to the reports directory.
|
|
26
|
+
await fs.rename(path.join(hiddenReportsPath, fileName), path.join(reportsPath, fileName));
|
|
27
|
+
// Update the data on the available reports.
|
|
28
|
+
await makeReportsExtract();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
27
31
|
// Return why.
|
|
28
32
|
return {
|
|
29
33
|
status: 'error',
|
|
30
|
-
message:
|
|
34
|
+
message: `Unhiding report file ${fileName} failed (${error.message})`
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
|
-
// Otherwise, i.e. if it succeeded, reverse the hiddenness property of the log.
|
|
34
|
-
log.hidden = false;
|
|
35
|
-
// Save the updated log.
|
|
36
|
-
await fs.writeFile(path.join(logsPath, `${jobName}.json`), getJSON(log));
|
|
37
37
|
}
|
|
38
38
|
// Otherwise, i.e. if the authorization code is invalid:
|
|
39
39
|
else {
|
|
@@ -44,51 +44,43 @@ exports.answer = async (_, search) => {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Get its
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// Otherwise, i.e. if it succeeded, add the report to the array.
|
|
65
|
-
reportSpecs.push({
|
|
47
|
+
// Initialize an array of data on reports to be unhidden.
|
|
48
|
+
const reportsData = [];
|
|
49
|
+
// Get the names of the hidden report files.
|
|
50
|
+
const hiddenReportFileNames = await fs.readdir(hiddenReportsPath);
|
|
51
|
+
// For each hidden report:
|
|
52
|
+
for (const reportFileName of hiddenReportFileNames) {
|
|
53
|
+
// Get its file.
|
|
54
|
+
const reportJSON = await fs.readFile(path.join(hiddenReportsPath, reportFileName), 'utf8');
|
|
55
|
+
// Get the report.
|
|
56
|
+
const report = JSON.parse(reportJSON);
|
|
57
|
+
const {id, target} = report;
|
|
58
|
+
const [timeStamp, jobID] = id.split('-');
|
|
59
|
+
const {what} = target;
|
|
60
|
+
// Add its data to the array.
|
|
61
|
+
reportsData.push({
|
|
66
62
|
what,
|
|
67
63
|
timeStamp,
|
|
68
|
-
jobID
|
|
69
|
-
hidden
|
|
64
|
+
jobID
|
|
70
65
|
});
|
|
71
66
|
}
|
|
72
|
-
// Sort the
|
|
73
|
-
|
|
67
|
+
// Sort the data by page name and then by time stamp.
|
|
68
|
+
reportsData.sort((a, b) => {
|
|
74
69
|
if (a.what === b.what) {
|
|
75
70
|
return a.timeStamp.localeCompare(b.timeStamp);
|
|
76
71
|
}
|
|
77
|
-
return a.what.localeCompare(b.what);
|
|
72
|
+
return a.what.localeCompare(b.what, 'en', {sensitivity: 'base'});
|
|
78
73
|
});
|
|
79
74
|
const lines = [];
|
|
80
75
|
const margin = ' '.repeat(12);
|
|
81
76
|
// For each report:
|
|
82
|
-
|
|
83
|
-
const {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
77
|
+
reportsData.forEach(data => {
|
|
78
|
+
const {jobID, timeStamp, what} = data;
|
|
79
|
+
const specString = `${what} (job <code>${jobID}</code> at ${timeStamp})`;
|
|
80
|
+
// Add a line with a radio button to unhide it.
|
|
81
|
+
lines.push(
|
|
82
|
+
`${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
|
|
83
|
+
);
|
|
92
84
|
});
|
|
93
85
|
const query = {
|
|
94
86
|
reports: lines.join('\n'),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {getReportData,
|
|
8
|
+
const {getReportData, makeReportsExtract, objectSort, reportsPath} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -20,12 +20,22 @@ exports.answer = async (_, search) => {
|
|
|
20
20
|
if (jobNames?.length) {
|
|
21
21
|
// If the authorization code is valid:
|
|
22
22
|
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
try {
|
|
24
|
+
// For each report to be deleted:
|
|
25
|
+
for (const jobName of jobNames) {
|
|
26
|
+
// Delete it.
|
|
27
|
+
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
28
|
+
}
|
|
29
|
+
// Update the extract of the available reports.
|
|
30
|
+
await makeReportsExtract();
|
|
31
|
+
}
|
|
32
|
+
// If this failed:
|
|
33
|
+
catch (error) {
|
|
34
|
+
// Return why.
|
|
35
|
+
return {
|
|
36
|
+
status: 'error',
|
|
37
|
+
message: `Deleting sole reports failed (${error.message})`
|
|
38
|
+
}
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
// Otherwise, i.e. if the authorization code is invalid:
|
|
@@ -43,8 +53,8 @@ exports.answer = async (_, search) => {
|
|
|
43
53
|
for (const reportName of reportNames) {
|
|
44
54
|
const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
|
|
45
55
|
// Get a summary of it.
|
|
46
|
-
const
|
|
47
|
-
const {error, issueCount, preventedToolCount, url} =
|
|
56
|
+
const reportFacts = await getReportData(timeStamp, jobID);
|
|
57
|
+
const {error, issueCount, preventedToolCount, url} = reportFacts;
|
|
48
58
|
// If this failed:
|
|
49
59
|
if (error) {
|
|
50
60
|
// Return why.
|
|
@@ -61,13 +71,9 @@ exports.answer = async (_, search) => {
|
|
|
61
71
|
url
|
|
62
72
|
});
|
|
63
73
|
}
|
|
64
|
-
// Sort the summaries
|
|
65
|
-
reportSpecs
|
|
66
|
-
|
|
67
|
-
return a.timeStamp.localeCompare(b.timeStamp);
|
|
68
|
-
}
|
|
69
|
-
return a.url.localeCompare(b.url);
|
|
70
|
-
});
|
|
74
|
+
// Sort the summaries primarily URL and secondarily by time stamp.
|
|
75
|
+
objectSort(reportSpecs, 'timeStamp', 'alpha');
|
|
76
|
+
objectSort(reportSpecs, 'url', 'alpha');
|
|
71
77
|
const lines = [];
|
|
72
78
|
const margin = ' '.repeat(12);
|
|
73
79
|
let anyDeletable = false;
|