@jrpool/kilotest 38.1.0 → 39.0.1

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.
Files changed (57) hide show
  1. package/.markdownlint-cli2.jsonc +12 -0
  2. package/.vscode/settings.json +3 -0
  3. package/CLAUDE.md +5 -0
  4. package/README.md +6 -4
  5. package/ai0BalanceForm/index.html +1 -1
  6. package/alerts.js +3 -1
  7. package/api/getReport.js +66 -0
  8. package/api/listDiagnoses.js +157 -0
  9. package/api/listIssues.js +225 -0
  10. package/api/listReports.js +87 -0
  11. package/api/listViolators.js +164 -0
  12. package/api/requestFeature.js +56 -0
  13. package/api/requestRetest.js +102 -0
  14. package/api/requestTest.js +106 -0
  15. package/api/routes.js +97 -0
  16. package/api/schemas.js +297 -0
  17. package/api/testSuite.js +294 -0
  18. package/api/util.js +149 -0
  19. package/diagnoses/index.js +2 -2
  20. package/docs/{AI-TOOL.md → AI-TOOLS.md} +54 -26
  21. package/docs/SERVICE.md +4 -1
  22. package/docs/decisions/001-api-design.md +59 -49
  23. package/eslint.config.mjs +13 -3
  24. package/index.html +1 -1
  25. package/index.js +202 -188
  26. package/issues/index.js +13 -21
  27. package/jsconfig.json +8 -0
  28. package/llms-full.txt +2 -2
  29. package/llms.txt +16 -11
  30. package/mcp.js +129 -65
  31. package/openapi.yaml +1234 -572
  32. package/package.json +18 -11
  33. package/reannotate/index.js +26 -15
  34. package/reannotateForm/index.js +15 -13
  35. package/redocly.yaml +4 -0
  36. package/reportHideForm/index.html +1 -1
  37. package/reportHideForm/index.js +24 -38
  38. package/reportIssue/index.js +3 -3
  39. package/reportIssues/index.js +128 -1
  40. package/reportUnhideForm/index.js +36 -44
  41. package/reportsExpungeForm/index.js +22 -16
  42. package/reportsPruneForm/index.js +22 -12
  43. package/reportsRewindForm/index.js +17 -9
  44. package/retestRec/index.js +8 -4
  45. package/retestRecForm/index.js +18 -6
  46. package/scripts/generate-openapi.js +83 -0
  47. package/targets/index.js +18 -13
  48. package/testOrder/index.js +11 -8
  49. package/tutorial/index.html +12 -12
  50. package/util.js +581 -539
  51. package/api/reportList.js +0 -83
  52. package/api/reportSummary.js +0 -181
  53. package/reportIssue/api.js +0 -183
  54. package/reportIssues/util.js +0 -138
  55. package/researchAgent.js +0 -206
  56. package/target/api.js +0 -126
  57. package/testRecForm/api.js +0 -55
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "38.1.0",
4
- "description": "An ensemble testing service with a focus on accessibility",
3
+ "version": "39.0.1",
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": "^1.0.0",
34
- "@eslint/js": "^10.0.1",
35
- "@eslint/json": "^1.1.0",
36
- "@eslint/markdown": "^7.5.1",
37
- "eslint": "^10.0.3",
38
- "globals": "^17.4.0"
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
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {annotateReport, getLogs, ruleIDs} = require('../util');
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 the logs of the latest reports per target.
19
- const targetsData = (await getLogs()).filter(log => ! log.superseded);
20
- // For each report:
21
- for (const targetData of targetsData) {
22
- const [timeStamp, jobID] = targetData.jobName.split('-');
23
- // Reannotate it.
24
- const annotationError = await annotateReport(ruleIDs, timeStamp, jobID);
25
- // If this failed:
26
- if (annotationError) {
27
- // Return an error page.
28
- return {
29
- status: 'error',
30
- message: annotationError
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.
@@ -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, getLogs, ruleIDs} = require('../util');
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
- const targetLogs = (await getLogs()).filter(log => ! log.superseded);
17
- const stillUnclassified = {};
16
+ // Initialize data on rule classification.
18
17
  const reClassified = {};
19
- // For each target:
20
- for (const targetLog of targetLogs) {
21
- const {jobName = '-'} = targetLog;
22
- // Get the latest report on it.
23
- const report = await getReport(... jobName.split('-'));
24
- const {acts = [], error} = report;
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(jobName);
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 (! issueID){
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(jobName);
56
+ stillUnclassified[which][ruleID].add(id);
55
57
  }
56
58
  });
57
59
  }
package/redocly.yaml ADDED
@@ -0,0 +1,4 @@
1
+ extends:
2
+ - recommended
3
+ rules:
4
+ operation-4xx-response: off
@@ -20,7 +20,7 @@
20
20
  __reports__
21
21
  </fieldset>
22
22
  <p><label>
23
- Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required>
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>
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getJSON, getLog, logsPath, reportsPath} = require('../util');
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
- // Get the log of the report.
24
- const log = await getLog(... jobName.split('-'));
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
- if (log.error) {
31
+ catch (error) {
27
32
  // Return why.
28
33
  return {
29
34
  status: 'error',
30
- message: log.error
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 reportFileName of reportFileNames) {
52
- const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-');
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 logs by page name and then by time stamp.
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 {hidden, what, timeStamp, jobID} = spec;
85
- // If it is not already hidden:
86
- if (! hidden) {
87
- const specString = `${what} (job <code>${jobID}</code> at ${timeStamp})`;
88
- // Add a line with a radio button to hide it.
89
- lines.push(
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'),
@@ -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 (! query.issue) {
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
- ... entry[1]
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 && ! ['HTML', 'HEAD', 'BODY', 'MAIN', 'NOSCRIPT'].includes(tagName)) {
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
  }
@@ -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 {getJSON, getLog, logsPath, reportsPath} = require('../util');
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
- // Get the log of the report.
24
- const log = await getLog(... jobName.split('-'));
25
- // If this failed:
26
- if (log.error) {
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: log.error
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
- const reportFileNames = await fs.readdir(reportsPath);
48
- // Initialize an array of report specifications.
49
- const reportSpecs = [];
50
- // For each report:
51
- for (const reportFileName of reportFileNames) {
52
- const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-');
53
- // Get its log.
54
- const log = await getLog(timeStamp, jobID);
55
- const {error, what, hidden} = log;
56
- // If this failed:
57
- if (error) {
58
- // Return why.
59
- return {
60
- status: 'error',
61
- message: error
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 logs by page name and then by time stamp.
73
- reportSpecs.sort((a, b) => {
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
- reportSpecs.forEach(spec => {
83
- const {hidden, what, timeStamp, jobID} = spec;
84
- // If it is hidden:
85
- if (hidden) {
86
- const specString = `${what} (job <code>${jobID}</code> at ${timeStamp})`;
87
- // Add a line with a radio button to unhide it.
88
- lines.push(
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, logsPath, reportsPath} = require('../util');
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
- // For each report to be deleted:
24
- for (const jobName of jobNames) {
25
- // Delete it.
26
- await fs.unlink(path.join(reportsPath, `${jobName}.json`));
27
- // Delete its log.
28
- await fs.unlink(path.join(logsPath, `${jobName}.json`));
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 reportSummary = await getReportData(timeStamp, jobID);
47
- const {error, issueCount, preventedToolCount, url} = reportSummary;
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 by URL and then by time stamp.
65
- reportSpecs.sort((a, b) => {
66
- if (a.url === b.url) {
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;