Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "eHelper",
"version": "1.2.6",
"version": "1.2.7",
"description": "eCampus 과목별 과제/퀴즈/강의/토론/자료를 한눈에 보는 확장 프로그램",
"permissions": ["storage", "downloads"],
"host_permissions": [
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ecampus-dashboard-smu",
"version": "1.2.6",
"version": "1.2.7",
"private": true,
"type": "module",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions public/page-dev-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,12 @@
devApi.openSettings = function openSettings() {
dispatchDevCommand('open-settings');
};

devApi.previewErrorLog = function previewErrorLog(text) {
dispatchDevCommand('preview-error-log', { text });
};

devApi.clearErrorLog = function clearErrorLog() {
dispatchDevCommand('clear-error-log');
};
})();
86 changes: 83 additions & 3 deletions src/content/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,45 @@
}));
}

function errorMessage(err) {
return String(err?.stack || err?.message || err || 'unknown error').trim();
}
Comment thread
Copilot marked this conversation as resolved.

function buildFailureLog(failures) {
const rows = Array.isArray(failures) ? failures : [];
if (!rows.length) return '';

return [
'[eHelper] eCampus 수집 실패 로그',
`time=${new Date().toLocaleString()}`,
`url=${location.href}`,
`failures=${rows.length}`,
'',
...rows.map((failure, index) => {
const course = failure?.course || {};
const snippets = Array.isArray(
E.__lastCourseCrawlHtml?.[String(course.courseId)],
)
? E.__lastCourseCrawlHtml[String(course.courseId)]
: [];
const html = snippets
.map(
(snippet) =>
`kind=${snippet.kind || '-'}\n----- HTML START -----\n${String(snippet.html || '').trim()}\n----- HTML END -----`,
)
.join('\n\n');
return [
`#${index + 1}`,
`courseId=${course.courseId || '-'}`,
`courseName=${course.courseName || '-'}`,
`reason=${failure?.reason || '-'}`,
`message=${failure?.message || '-'}`,
html ? `html=\n${html}` : 'html=-',
].join('\n');
}),
].join('\n');
}

async function crawlCoursesWithResults(courses) {
if (typeof E.crawlCoursesWithConcurrency === 'function') {
const results = await E.crawlCoursesWithConcurrency(courses);
Expand All @@ -235,9 +274,10 @@
E.constants.CRAWL_CONCURRENCY,
async (course) => {
try {
const items = await E.crawlCourseItems(course);
return {
course,
items: await E.crawlCourseItems(course),
items: Array.isArray(items) ? items : [],
ok: true,
};
} catch (err) {
Expand All @@ -249,6 +289,8 @@
course,
items: [],
ok: false,
reason: 'exception',
message: errorMessage(err),
};
}
},
Expand Down Expand Up @@ -312,6 +354,7 @@
reusedCourseCount: 0,
crawledCourseCount: 0,
failedCourseCount: 0,
failures: [],
};
}

Expand Down Expand Up @@ -354,6 +397,7 @@

const crawledItems = [];
let failedCourseCount = 0;
const failures = [];

if (coursesToCrawl.length) {
const results = await crawlCoursesWithResults(coursesToCrawl);
Expand All @@ -367,8 +411,27 @@
if (!courseId) return;

handledCourseIds.add(courseId);
if (result?.ok === false) {
const resultItems = Array.isArray(result?.items)
? result.items
: [];
const failureReason =
E.__lastCourseCrawlFailureReason?.[courseId] ||
result.reason ||
'';
if (result?.ok === false || failureReason) {
failedCourseCount += 1;
failures.push({
course,
reason: failureReason || 'crawl_failed',
message:
result.message ||
(failureReason === 'moodle_token_missing'
? '설정에서 eCampus API 로그인이 필요함'
: failureReason === 'course_html_failed'
? '과목 활동 HTML을 불러오지 못함'
: '') ||
'과목 수집 실패',
});
crawledItems.push(
...bindItemsToCourse(
snapshotItemsByCourse.get(courseId),
Expand All @@ -379,14 +442,19 @@
}

nextRunMap[courseId] = now;
crawledItems.push(...bindItemsToCourse(result?.items, course));
crawledItems.push(...bindItemsToCourse(resultItems, course));
});

coursesToCrawl.forEach((course) => {
const courseId = String(course?.courseId || '').trim();
if (!courseId || handledCourseIds.has(courseId)) return;

failedCourseCount += 1;
failures.push({
course,
reason: 'missing_result',
message: '과목 수집 결과 누락',
});
crawledItems.push(
...bindItemsToCourse(snapshotItemsByCourse.get(courseId), course),
);
Expand All @@ -402,6 +470,7 @@
reusedCourseCount,
crawledCourseCount: coursesToCrawl.length,
failedCourseCount,
failures,
};
}

Expand Down Expand Up @@ -541,6 +610,7 @@
? 'CACHE'
: 'OK',
);
E.setErrorLog?.(buildFailureLog(refreshed.failures));
E.setSub(summarizeRefreshOutcome(visibleItems, refreshed));
E.render(visibleItems);
return;
Expand Down Expand Up @@ -678,11 +748,21 @@
? 'CACHE'
: 'OK',
);
E.setErrorLog?.(buildFailureLog(refreshed.failures));
E.setSub(summarizeRefreshOutcome(mergedItems, refreshed));
E.render(mergedItems);
} catch (e) {
console.error(e);
E.setBadge('ERR');
E.setErrorLog?.(
buildFailureLog([
{
course: getCurrentCourse() || {},
reason: 'refresh_exception',
message: errorMessage(e),
},
]),
);
E.setSub(
'크롤링 중 오류가 발생했어요. (로그인 상태/권한/네트워크 확인)',
);
Expand Down
1 change: 1 addition & 0 deletions src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import './modules/shared';
import './modules/vod';
import './modules/vod.panel';
import './modules/crawler.course';
import './modules/moodle.api';
import './modules/crawler';
import './modules/crawler.meta';
import './modules/crawler.activity';
Expand Down
127 changes: 126 additions & 1 deletion src/content/modules/crawler.course-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,94 @@
1,
Number(E.constants?.DETAIL_ENRICH_CONCURRENCY || 2),
);
E.__lastCourseCrawlHtml = E.__lastCourseCrawlHtml || {};
E.__lastCourseCrawlFailureReason =
E.__lastCourseCrawlFailureReason || {};
E.__lastCourseCrawlHtml[String(courseId)] = [];
delete E.__lastCourseCrawlFailureReason[String(courseId)];
let courseHubFailed = false;
const saveHtmlSnippet = (kind, html, selectors = []) => {
E.__lastCourseCrawlHtml = E.__lastCourseCrawlHtml || {};
const key = String(courseId);
const snippets = (E.__lastCourseCrawlHtml[key] =
E.__lastCourseCrawlHtml[key] || []);
if (
snippets.length >= 6 ||
snippets.filter((snippet) => snippet?.kind === kind).length >= 2
) {
return;
}
const doc = new DOMParser().parseFromString(
String(html || ''),
'text/html',
);
const node = selectors
.map((selector) => doc.querySelector(selector))
.find(Boolean);
snippets.push({
kind,
html: String(node?.outerHTML || html || '').slice(0, 20000),
});
};
const crawlApiFallback = async () => {
if (typeof E.fetchMoodleCourseContents !== 'function') return [];

const contents = await E.fetchMoodleCourseContents(courseId);
if (!Array.isArray(contents) || !contents.length) return [];

const [assignments, quizzes, lectures, resources] =
await Promise.all([
typeof E.fetchMoodleAssignmentItems === 'function'
? E.fetchMoodleAssignmentItems({
courseId,
courseName: normalizedCourseName,
courseIsNew: normalizedCourseIsNew,
})
: Promise.resolve([]),
typeof E.fetchMoodleQuizItems === 'function'
? E.fetchMoodleQuizItems({
courseId,
courseName: normalizedCourseName,
courseIsNew: normalizedCourseIsNew,
contents,
})
: Promise.resolve([]),
typeof E.fetchMoodleLectureItems === 'function'
? E.fetchMoodleLectureItems({
courseId,
courseName: normalizedCourseName,
courseIsNew: normalizedCourseIsNew,
contents,
})
: Promise.resolve([]),
!shouldSkipResources &&
typeof E.fetchMoodleResourceItems === 'function'
? E.fetchMoodleResourceItems({
courseId,
courseName: normalizedCourseName,
courseIsNew: normalizedCourseIsNew,
contents,
})
: Promise.resolve([]),
]);

return E.dedupeItems([
...assignments,
...quizzes,
...lectures,
...resources,
]);
};
const crawlAssignmentItems = async () => {
try {
const assignHtml = await E.fetchHtml(
`/mod/assign/index.php?id=${courseId}`,
);
saveHtmlSnippet('assignment:index', assignHtml, [
'table.generaltable',
'#region-main',
'[role="main"]',
]);
const assignItems = E.parseAssignIndexHtml(
assignHtml,
courseId,
Expand Down Expand Up @@ -76,6 +159,11 @@
const quizHtml = await E.fetchHtml(
`/mod/quiz/index.php?id=${courseId}`,
);
saveHtmlSnippet('quiz:index', quizHtml, [
'table.generaltable',
'#region-main',
'[role="main"]',
]);
const quizItems = E.parseQuizIndexHtml(
quizHtml,
courseId,
Expand Down Expand Up @@ -130,6 +218,11 @@
const courseHtml = await E.fetchHtml(
`/course/view.php?id=${courseId}`,
);
saveHtmlSnippet('course:activities', courseHtml, [
'.course-content',
'#region-main',
'[role="main"]',
]);
courseDoc = new DOMParser().parseFromString(
courseHtml,
'text/html',
Expand Down Expand Up @@ -203,6 +296,11 @@
async (reportUrl) => {
try {
const reportHtml = await E.fetchHtml(reportUrl);
saveHtmlSnippet('lecture:report', reportHtml, [
'table',
'#region-main',
'[role="main"]',
]);
const reportMap =
E.parseStatusRowsFromReportHtml(reportHtml);
const rowsCount = Array.isArray(reportMap.rows)
Expand Down Expand Up @@ -358,6 +456,11 @@
...noticeItems,
);
} catch (err) {
courseHubFailed = true;
E.__lastCourseCrawlFailureReason =
E.__lastCourseCrawlFailureReason || {};
E.__lastCourseCrawlFailureReason[String(courseId)] =
'course_html_failed';
console.warn(
`[ECDASH] course hub crawl failed. courseId=${courseId} (${normalizedCourseName})`,
err,
Expand All @@ -371,6 +474,28 @@

all.push(...assignmentItems, ...quizItems);

return E.dedupeItems(all);
const items = E.dedupeItems(all);
if (items.length) return items;
if (!courseHubFailed) return [];

try {
const apiItems = await crawlApiFallback();
if (apiItems.length) {
delete E.__lastCourseCrawlFailureReason[String(courseId)];
}
return apiItems;
} catch (err) {
if (/Moodle token missing/i.test(String(err?.message || err))) {
E.__lastCourseCrawlFailureReason =
E.__lastCourseCrawlFailureReason || {};
E.__lastCourseCrawlFailureReason[String(courseId)] =
'moodle_token_missing';
}
console.debug(
`[ECDASH] Moodle API fallback failed. courseId=${courseId}`,
err,
);
return [];
}
};
})();
Loading
Loading