|
|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
import { ApiClient } from "../api-client.js";
|
|
|
|
|
import { ApiClient, type Issue } from "../api-client.js";
|
|
|
|
|
import { t } from "../i18n/index.js";
|
|
|
|
|
|
|
|
|
|
export async function getIssues(
|
|
|
|
|
@ -7,16 +7,155 @@ export async function getIssues(
|
|
|
|
|
lang: "en" | "ko",
|
|
|
|
|
category?: string,
|
|
|
|
|
severity?: string,
|
|
|
|
|
pageUrl?: string,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
// Try single-page first
|
|
|
|
|
try {
|
|
|
|
|
const result = await client.getIssues(id, category, severity);
|
|
|
|
|
return formatSingleIssues(result.inspection_id, result.issues, result.total, result.filters, lang);
|
|
|
|
|
} catch {
|
|
|
|
|
// Might be a site inspection ID — aggregate issues from all pages
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return await formatSiteIssues(client, id, lang, category, severity, pageUrl);
|
|
|
|
|
} catch {
|
|
|
|
|
return t("get_inspection.not_found", lang, { id });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function formatSiteIssues(
|
|
|
|
|
client: ApiClient,
|
|
|
|
|
siteId: string,
|
|
|
|
|
lang: "en" | "ko",
|
|
|
|
|
category?: string,
|
|
|
|
|
severity?: string,
|
|
|
|
|
pageUrl?: string,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const site = await client.getSiteInspection(siteId);
|
|
|
|
|
|
|
|
|
|
// Collect pages with inspection results
|
|
|
|
|
let pages = site.discovered_pages.filter(
|
|
|
|
|
(p) => p.inspection_id && p.status === "completed",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Filter by page URL if specified
|
|
|
|
|
if (pageUrl) {
|
|
|
|
|
pages = pages.filter((p) => p.url.includes(pageUrl));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pages.length === 0) {
|
|
|
|
|
const lines = [
|
|
|
|
|
`# ${t("get_issues.title", lang)} (Site)`,
|
|
|
|
|
`**Site**: ${site.root_url}`,
|
|
|
|
|
"",
|
|
|
|
|
"No completed page inspections found.",
|
|
|
|
|
];
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch issues from each page (parallel, max 10 concurrent)
|
|
|
|
|
type PageIssues = { url: string; issues: Issue[] };
|
|
|
|
|
const pageIssues: PageIssues[] = [];
|
|
|
|
|
|
|
|
|
|
const chunks = chunkArray(pages, 10);
|
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
|
const results = await Promise.allSettled(
|
|
|
|
|
chunk.map(async (p) => {
|
|
|
|
|
const r = await client.getIssues(p.inspection_id!, category, severity);
|
|
|
|
|
return { url: p.url, issues: r.issues } as PageIssues;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
for (const r of results) {
|
|
|
|
|
if (r.status === "fulfilled" && r.value.issues.length > 0) {
|
|
|
|
|
pageIssues.push(r.value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Aggregate all issues
|
|
|
|
|
const allIssues = pageIssues.flatMap((p) =>
|
|
|
|
|
p.issues.map((i) => ({ ...i, _pageUrl: p.url })),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const lines: string[] = [];
|
|
|
|
|
lines.push(`# ${t("get_issues.title", lang)}`);
|
|
|
|
|
lines.push(`**Inspection ID**: ${result.inspection_id}`);
|
|
|
|
|
lines.push(`**${t("get_issues.total", lang)}**: ${result.total}`);
|
|
|
|
|
lines.push(`# ${t("get_issues.title", lang)} (Site)`);
|
|
|
|
|
lines.push(`**Site**: ${site.root_url} (${site.domain})`);
|
|
|
|
|
lines.push(`**Pages**: ${pages.length}`);
|
|
|
|
|
lines.push(`**${t("get_issues.total", lang)}**: ${allIssues.length}`);
|
|
|
|
|
|
|
|
|
|
// Show applied filters
|
|
|
|
|
const activeFilters = Object.entries(result.filters)
|
|
|
|
|
const filters: string[] = [];
|
|
|
|
|
if (category) filters.push(`category: ${category}`);
|
|
|
|
|
if (severity) filters.push(`severity: ${severity}`);
|
|
|
|
|
if (pageUrl) filters.push(`page: ${pageUrl}`);
|
|
|
|
|
if (filters.length > 0) {
|
|
|
|
|
lines.push(`**${t("get_issues.filters", lang)}**: ${filters.join(", ")}`);
|
|
|
|
|
}
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
if (allIssues.length === 0) {
|
|
|
|
|
lines.push("No issues found.");
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Summary by severity
|
|
|
|
|
const sevCounts = { critical: 0, major: 0, minor: 0, info: 0 };
|
|
|
|
|
for (const i of allIssues) sevCounts[i.severity]++;
|
|
|
|
|
lines.push(
|
|
|
|
|
`**Summary**: Critical: ${sevCounts.critical}, Major: ${sevCounts.major}, Minor: ${sevCounts.minor}, Info: ${sevCounts.info}`,
|
|
|
|
|
);
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
// Per-page breakdown table
|
|
|
|
|
lines.push("## Pages");
|
|
|
|
|
lines.push("| Page | Issues | Critical | Major |");
|
|
|
|
|
lines.push("|---|---|---|---|");
|
|
|
|
|
for (const pi of pageIssues) {
|
|
|
|
|
const crit = pi.issues.filter((i) => i.severity === "critical").length;
|
|
|
|
|
const maj = pi.issues.filter((i) => i.severity === "major").length;
|
|
|
|
|
const shortUrl = pi.url.length > 60 ? pi.url.slice(0, 57) + "..." : pi.url;
|
|
|
|
|
lines.push(`| ${shortUrl} | ${pi.issues.length} | ${crit} | ${maj} |`);
|
|
|
|
|
}
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
// Top issues across all pages (critical + major first, max 15)
|
|
|
|
|
const sorted = allIssues
|
|
|
|
|
.sort((a, b) => sevOrder(a.severity) - sevOrder(b.severity))
|
|
|
|
|
.slice(0, 15);
|
|
|
|
|
|
|
|
|
|
lines.push("## Top Issues");
|
|
|
|
|
for (let i = 0; i < sorted.length; i++) {
|
|
|
|
|
const issue = sorted[i];
|
|
|
|
|
const shortUrl = issue._pageUrl.replace(site.root_url, "") || "/";
|
|
|
|
|
lines.push(
|
|
|
|
|
`### ${i + 1}. [${issue.severity.toUpperCase()}] ${issue.code} — ${shortUrl}`,
|
|
|
|
|
);
|
|
|
|
|
lines.push(`- **${t("result.message", lang)}**: ${issue.message}`);
|
|
|
|
|
if (issue.element) {
|
|
|
|
|
const el = issue.element.length > 120 ? issue.element.slice(0, 117) + "..." : issue.element;
|
|
|
|
|
lines.push(`- **${t("result.element", lang)}**: \`${el}\``);
|
|
|
|
|
}
|
|
|
|
|
lines.push(`- **${t("result.suggestion", lang)}**: ${issue.suggestion}`);
|
|
|
|
|
lines.push("");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatSingleIssues(
|
|
|
|
|
inspectionId: string,
|
|
|
|
|
issues: Issue[],
|
|
|
|
|
total: number,
|
|
|
|
|
filters: Record<string, string | null>,
|
|
|
|
|
lang: "en" | "ko",
|
|
|
|
|
): string {
|
|
|
|
|
const lines: string[] = [];
|
|
|
|
|
lines.push(`# ${t("get_issues.title", lang)}`);
|
|
|
|
|
lines.push(`**Inspection ID**: ${inspectionId}`);
|
|
|
|
|
lines.push(`**${t("get_issues.total", lang)}**: ${total}`);
|
|
|
|
|
|
|
|
|
|
const activeFilters = Object.entries(filters)
|
|
|
|
|
.filter(([, v]) => v)
|
|
|
|
|
.map(([k, v]) => `${k}: ${v}`)
|
|
|
|
|
.join(", ");
|
|
|
|
|
@ -25,7 +164,7 @@ export async function getIssues(
|
|
|
|
|
}
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
if (result.issues.length === 0) {
|
|
|
|
|
if (issues.length === 0) {
|
|
|
|
|
lines.push("No issues found.");
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
}
|
|
|
|
|
@ -36,8 +175,8 @@ export async function getIssues(
|
|
|
|
|
);
|
|
|
|
|
lines.push("|---|---|---|---|---|");
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < result.issues.length; i++) {
|
|
|
|
|
const issue = result.issues[i];
|
|
|
|
|
for (let i = 0; i < issues.length; i++) {
|
|
|
|
|
const issue = issues[i];
|
|
|
|
|
const msg =
|
|
|
|
|
issue.message.length > 80
|
|
|
|
|
? issue.message.slice(0, 77) + "..."
|
|
|
|
|
@ -49,10 +188,10 @@ export async function getIssues(
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
// Detailed view for first 10
|
|
|
|
|
const detailCount = Math.min(result.issues.length, 10);
|
|
|
|
|
const detailCount = Math.min(issues.length, 10);
|
|
|
|
|
lines.push("## Details");
|
|
|
|
|
for (let i = 0; i < detailCount; i++) {
|
|
|
|
|
const issue = result.issues[i];
|
|
|
|
|
const issue = issues[i];
|
|
|
|
|
lines.push(`### ${i + 1}. [${issue.severity.toUpperCase()}] ${issue.code}`);
|
|
|
|
|
lines.push(`- **${t("result.message", lang)}**: ${issue.message}`);
|
|
|
|
|
if (issue.element) {
|
|
|
|
|
@ -69,3 +208,15 @@ export async function getIssues(
|
|
|
|
|
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sevOrder(s: string): number {
|
|
|
|
|
return { critical: 0, major: 1, minor: 2, info: 3 }[s] ?? 4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function chunkArray<T>(arr: T[], size: number): T[][] {
|
|
|
|
|
const chunks: T[][] = [];
|
|
|
|
|
for (let i = 0; i < arr.length; i += size) {
|
|
|
|
|
chunks.push(arr.slice(i, i + size));
|
|
|
|
|
}
|
|
|
|
|
return chunks;
|
|
|
|
|
}
|
|
|
|
|
|