feat: enhance asset import functionality and unit tracking

- Added WHISPER_URL environment variable to docker-compose for audio transcription service.
- Updated Nginx configuration to increase timeout settings for API requests.
- Enhanced asset ingestion process to extract unit numbers from ZIP entry paths, supporting various naming conventions.
- Implemented logic to split intensive courses into two regular courses during asset import.
- Added new fields to the Asset and QuestionBank models to track unit numbers and source asset links.
- Introduced backward-compatible fallbacks for fetching study plans and courses from legacy MySQL database.
- Improved error handling and progress tracking during ZIP file uploads in the frontend.
- Created a new SQL migration to add unit_number and source_asset_id columns to the assets and question_bank tables, along with necessary indexes for performance.
This commit is contained in:
2026-04-07 13:38:22 -04:00
parent 7f9b9d69ae
commit 024bd6e46d
11 changed files with 687 additions and 101 deletions
+26 -3
View File
@@ -974,6 +974,10 @@ export const cmsApi = {
englishLevel?: string,
samPlanId?: number,
samCourseId?: number,
onProgress?: (pct: number) => void,
splitToRegular = false,
samCourseIdR1?: number,
samCourseIdR2?: number,
): Promise<AssetZipImportResult> => {
return new Promise((resolve, reject) => {
const formData = new FormData();
@@ -983,6 +987,11 @@ export const cmsApi = {
if (englishLevel) formData.append('english_level', englishLevel);
if (samPlanId) formData.append('sam_plan_id', String(samPlanId));
if (samCourseId) formData.append('sam_course_id', String(samCourseId));
if (splitToRegular) {
formData.append('split_to_regular', 'true');
if (samCourseIdR1) formData.append('sam_course_id_r1', String(samCourseIdR1));
if (samCourseIdR2) formData.append('sam_course_id_r2', String(samCourseIdR2));
}
const xhr = new XMLHttpRequest();
xhr.open('POST', `${API_BASE_URL}/api/assets/import-zip`);
@@ -996,15 +1005,29 @@ export const cmsApi = {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
let msg = 'ZIP import failed';
let msg = `ZIP import failed (HTTP ${xhr.status})`;
try {
msg = JSON.parse(xhr.responseText).message || msg;
} catch { }
const parsed = JSON.parse(xhr.responseText);
msg = parsed.message || parsed.error || msg;
} catch {
const raw = (xhr.responseText || '').trim();
if (raw) {
const compact = raw.replace(/\s+/g, ' ').slice(0, 240);
msg = `${msg}: ${compact}`;
}
}
reject(new Error(msg));
}
};
xhr.onerror = () => reject(new Error('Network error'));
if (onProgress) {
xhr.upload.onprogress = (event) => {
if (!event.lengthComputable) return;
const pct = Math.round((event.loaded / event.total) * 100);
onProgress(Math.max(0, Math.min(100, pct)));
};
}
xhr.send(formData);
});
},