Update Saqel Platform: 2026-09-02 00:55:56
This commit is contained in:
@@ -25,27 +25,6 @@ class GuardianController
|
||||
[$guardianId]
|
||||
);
|
||||
|
||||
// If no children linked (for demo purposes, link up to 3 students so the multi-child UI can be tested)
|
||||
if (empty($children)) {
|
||||
$demoStudents = Database::select("SELECT id FROM students LIMIT 3");
|
||||
if (!empty($demoStudents)) {
|
||||
foreach ($demoStudents as $demoStudent) {
|
||||
Database::insert(
|
||||
"INSERT IGNORE INTO guardian_students (guardian_id, student_id, relationship_type) VALUES (?, ?, 'father')",
|
||||
[$guardianId, $demoStudent['id']]
|
||||
);
|
||||
}
|
||||
// Re-fetch
|
||||
$children = Database::select(
|
||||
"SELECT s.id, s.uuid, s.full_name, s.national_id, s.grade_level, s.stream
|
||||
FROM students s
|
||||
JOIN guardian_students gs ON gs.student_id = s.id
|
||||
WHERE gs.guardian_id = ?",
|
||||
[$guardianId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$dashboardData = [];
|
||||
|
||||
foreach ($children as $child) {
|
||||
|
||||
@@ -400,67 +400,9 @@ class VideoController
|
||||
}
|
||||
|
||||
if (!$lesson) {
|
||||
// Construct a complete, standalone live playback payload directly from the requested lesson
|
||||
$cleanTitle = str_replace(['_', '-'], ' ', $rawId);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'lesson' => [
|
||||
'id' => 1,
|
||||
'course_id' => 0,
|
||||
'title' => !empty($rawId) ? $cleanTitle : 'الدرس التفاعلي المعتمد',
|
||||
'duration_seconds' => 900,
|
||||
'is_free_preview' => true,
|
||||
'storage_type' => 'api_upload'
|
||||
],
|
||||
'playback' => [
|
||||
'storage_type' => 'api_upload',
|
||||
'stream_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
|
||||
'hls_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
|
||||
'video_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
|
||||
'is_direct' => true
|
||||
],
|
||||
'available_versions' => [
|
||||
[
|
||||
'lesson_id' => 1,
|
||||
'is_ai' => true,
|
||||
'teacher_name' => 'الذكاء الاصطناعي الوزاري',
|
||||
'school_name' => 'وزارة التربية والتعليم',
|
||||
'label' => 'شرح الذكاء الاصطناعي المعتمد 🤖',
|
||||
'is_recommended' => true,
|
||||
'playback' => [
|
||||
'video_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson'
|
||||
]
|
||||
]
|
||||
],
|
||||
'chapters' => [],
|
||||
'checkpoints' => [
|
||||
[
|
||||
'exam_id' => 1,
|
||||
'timestamp_seconds' => 15,
|
||||
'rewind_on_fail_seconds' => 30,
|
||||
'question_text' => 'فحص الفهم السقراطي: ما هو المفهوم الأساسي المشروح في هذا الدرس؟',
|
||||
'explanation' => 'التركيز على نتاجات التعلم يضمن استيعاب المتطلبات الوزارية بدقة.',
|
||||
'options' => [
|
||||
['id' => 1, 'text' => 'تطبيق القواعد والمفاهيم العلمية/الرياضية بدقة', 'is_correct' => true],
|
||||
['id' => 2, 'text' => 'تجاهل الخطوات التحليلية', 'is_correct' => false],
|
||||
['id' => 3, 'text' => 'الحفظ المجرد دون فهم سياقي', 'is_correct' => false],
|
||||
]
|
||||
],
|
||||
[
|
||||
'exam_id' => 2,
|
||||
'timestamp_seconds' => 45,
|
||||
'rewind_on_fail_seconds' => 25,
|
||||
'question_text' => 'فحص الفهم التعمقي: كيف يتم التحقق من صحة الحل وتجنب الأخطاء الشائعة؟',
|
||||
'explanation' => 'التعويض المباشر والتحليل المنطقي يثبتان صحة النتائج.',
|
||||
'options' => [
|
||||
['id' => 4, 'text' => 'بالتعويض المباشر في المسألة الأصلية والتحقق من التكافؤ', 'is_correct' => true],
|
||||
['id' => 5, 'text' => 'بالاعتماد على التخمين', 'is_correct' => false],
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
$response->status(404)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'الدرس غير موجود أو لم يتم نشر الفيديو الخاص به بعد'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
@@ -497,7 +439,8 @@ class VideoController
|
||||
}
|
||||
|
||||
$checkpoints[] = [
|
||||
'exam_id' => (int)$ex['exam_id'],
|
||||
'exam_id' => (int)$ex['exam_id'],
|
||||
'question_id' => $q ? (int)$q['id'] : 0,
|
||||
'timestamp_seconds' => (int)$ex['timestamp_seconds'],
|
||||
'rewind_on_fail_seconds' => (int)$ex['rewind_on_fail_seconds'],
|
||||
'question_text' => $q['question_text'] ?? 'سؤال فحص فهم الفكرة:',
|
||||
@@ -512,34 +455,6 @@ class VideoController
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($checkpoints)) {
|
||||
$checkpoints = [
|
||||
[
|
||||
'exam_id' => 1,
|
||||
'timestamp_seconds' => 15,
|
||||
'rewind_on_fail_seconds' => 30,
|
||||
'question_text' => 'فحص الفهم الأولي: ما هو المفهوم الأساسي المشروح في هذه الفقرة من الدرس؟',
|
||||
'explanation' => 'التركيز على نتاجات التعلم يضمن استيعاب المتطلبات الوزارية بدقة.',
|
||||
'options' => [
|
||||
['id' => 1, 'text' => 'تطبيق القواعد والمفاهيم العلمية/الرياضية بدقة', 'is_correct' => true],
|
||||
['id' => 2, 'text' => 'تجاهل الخطوات التحليلية', 'is_correct' => false],
|
||||
['id' => 3, 'text' => 'الحفظ المجرد دون فهم سياقي', 'is_correct' => false],
|
||||
]
|
||||
],
|
||||
[
|
||||
'exam_id' => 2,
|
||||
'timestamp_seconds' => 45,
|
||||
'rewind_on_fail_seconds' => 25,
|
||||
'question_text' => 'فحص الفهم التعمقي: كيف يتم التحقق من صحة الحل وتجنب الأخطاء الشائعة؟',
|
||||
'explanation' => 'التعويض المباشر والتحليل المنطقي يثبتان صحة النتائج.',
|
||||
'options' => [
|
||||
['id' => 4, 'text' => 'بالتعويض المباشر في المسألة الأصلية والتحقق من التكافؤ', 'is_correct' => true],
|
||||
['id' => 5, 'text' => 'بالاعتماد على التخمين', 'is_correct' => false],
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$storageType = $lesson['storage_type'] ?? 'bunny_stream';
|
||||
$playbackInfo = [];
|
||||
|
||||
@@ -553,7 +468,11 @@ class VideoController
|
||||
];
|
||||
} else {
|
||||
// Bunny Stream Signed Playback
|
||||
$bunnyId = $lesson['bunny_video_id'] ?: 'mock-bunny-guid-2026';
|
||||
$bunnyId = $lesson['bunny_video_id'] ?: '';
|
||||
if ($bunnyId === '') {
|
||||
$response->status(409)->json(['status' => 'error', 'message' => 'الفيديو لم يجهز للبث بعد']);
|
||||
return;
|
||||
}
|
||||
$signedData = VideoService::generateBunnySignedPlayback($bunnyId, 10800); // 3-hour token
|
||||
$playbackInfo = array_merge(['storage_type' => 'bunny_stream'], $signedData);
|
||||
}
|
||||
@@ -587,7 +506,8 @@ class VideoController
|
||||
'video_url' => $ver['ai_video_url'] ?: ($ver['hls_url'] ?: ('/api/videos/stream/' . $ver['video_uuid']))
|
||||
];
|
||||
} else {
|
||||
$bId = $ver['bunny_video_id'] ?: 'mock-bunny-guid-2026';
|
||||
$bId = $ver['bunny_video_id'] ?: '';
|
||||
if ($bId === '') continue;
|
||||
$signed = VideoService::generateBunnySignedPlayback($bId, 10800);
|
||||
$vPlayback = array_merge(['storage_type' => 'bunny_stream', 'video_url' => $signed['hls_url']], $signed);
|
||||
}
|
||||
@@ -637,6 +557,38 @@ class VideoController
|
||||
]);
|
||||
}
|
||||
|
||||
/** Save real playback progress; the client never owns the readiness score. */
|
||||
public function saveProgress(Request $request, Response $response): void
|
||||
{
|
||||
VideoService::ensureSchema();
|
||||
$lessonId = (int)$request->getParam('id');
|
||||
$body = $request->getBody();
|
||||
$position = max(0, (int)($body['position_seconds'] ?? 0));
|
||||
$watched = max(0, (int)($body['watched_seconds'] ?? $position));
|
||||
$lesson = Database::selectOne('SELECT id, duration_seconds FROM lessons WHERE id = ? LIMIT 1', [$lessonId]);
|
||||
if (!$lesson) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'الدرس غير موجود']);
|
||||
return;
|
||||
}
|
||||
|
||||
$duration = max(1, (int)$lesson['duration_seconds']);
|
||||
$percentage = min(100, round(($position / $duration) * 100, 2));
|
||||
$completed = $percentage >= 90 ? 1 : 0;
|
||||
Database::query(
|
||||
"INSERT INTO lesson_progress (student_id, lesson_id, position_seconds, watched_seconds, completion_percentage, is_completed, last_seen_at, completed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW(), CASE WHEN ? = 1 THEN NOW() ELSE NULL END)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
position_seconds = VALUES(position_seconds),
|
||||
watched_seconds = GREATEST(watched_seconds, VALUES(watched_seconds)),
|
||||
completion_percentage = VALUES(completion_percentage),
|
||||
is_completed = GREATEST(is_completed, VALUES(is_completed)),
|
||||
last_seen_at = NOW(),
|
||||
completed_at = CASE WHEN is_completed = 1 OR VALUES(is_completed) = 1 THEN COALESCE(completed_at, NOW()) ELSE completed_at END",
|
||||
[$request->user_id, $lessonId, $position, $watched, $percentage, $completed, $completed]
|
||||
);
|
||||
$response->json(['status' => 'success', 'data' => ['completion_percentage' => $percentage, 'is_completed' => (bool)$completed]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook listener for Bunny Stream encoding notifications
|
||||
* POST /api/webhooks/bunny
|
||||
|
||||
Reference in New Issue
Block a user