diff --git a/backend/app/Controllers/VideoController.php b/backend/app/Controllers/VideoController.php
index c3afd3b..b8ac842 100644
--- a/backend/app/Controllers/VideoController.php
+++ b/backend/app/Controllers/VideoController.php
@@ -297,6 +297,35 @@ class VideoController
]);
}
+ /**
+ * List all published lessons for student portal
+ * GET /api/student/lessons
+ */
+ public function getStudentLessons(Request $request, Response $response): void
+ {
+ VideoService::ensureSchema();
+ CurriculumService::ensureSchema();
+
+ $courseId = (int)($request->getQuery('course_id') ?: 0);
+ $where = $courseId > 0 ? "WHERE l.course_id = {$courseId}" : "";
+
+ $lessons = Database::select(
+ "SELECT l.id, l.course_id, l.title, l.sequence_order, l.storage_type, l.video_uuid, l.hls_url, l.thumbnail_url,
+ l.duration_seconds, l.is_free_preview, l.created_at,
+ c.title as course_title,
+ (SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count
+ FROM lessons l
+ JOIN courses c ON l.course_id = c.id
+ {$where}
+ ORDER BY l.id DESC"
+ );
+
+ $response->json([
+ 'status' => 'success',
+ 'data' => $lessons
+ ]);
+ }
+
/**
* Get Lesson Playback Data with Chapters Roadmap and Socratic Checkpoints
* GET /api/lessons/{id}/playback
@@ -307,16 +336,23 @@ class VideoController
CurriculumService::ensureSchema();
$lessonId = (int)$request->getParam('id');
- if (!$lessonId) {
- $response->status(400)->json(['status' => 'error', 'message' => 'معرف الدرس مطلوب']);
+ $lesson = null;
+
+ if ($lessonId > 0) {
+ $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]);
+ }
+
+ if (!$lesson) {
+ // Fallback to latest available lesson
+ $lesson = Database::selectOne("SELECT * FROM lessons ORDER BY id DESC LIMIT 1");
+ }
+
+ if (!$lesson) {
+ $response->status(404)->json(['status' => 'error', 'message' => 'لا توجد دروس متاحة حالياً']);
return;
}
- $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]);
- if (!$lesson) {
- $response->status(404)->json(['status' => 'error', 'message' => 'الدرس غير موجود']);
- return;
- }
+ $lessonId = (int)$lesson['id'];
// If lesson has no checkpoints yet, generate them autonomously
$existingCount = Database::selectOne("SELECT COUNT(*) as cnt FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]);
diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php
index cb22cbf..6202956 100644
--- a/backend/app/Views/StudentPortal.php
+++ b/backend/app/Views/StudentPortal.php
@@ -600,13 +600,24 @@ class StudentPortal
+
+
+
+ 📚 فهرس الحصص والدروس المتاحة:
+ جارٍ التحميل...
+
+
+
+
+
+
-
الدرس 4: قواعد الاشتقاق الأساسية — رياضيات علمي
- الأستاذ حمزة الغويري • Bunny Stream DRM Protection
+ الدرس: قواعد الاشتقاق والتحليل الجنائي
+ الأستاذ حمزة الغويري • Cloudflare R2 + HLS Adaptive Stream
-
- Checkpoint Active (00:15)
+
+ الذكاء الاصطناعي السقراطي نشط 🧠
@@ -1048,6 +1059,7 @@ class StudentPortal
let activeLessonCheckpoints = [];
let triggeredCheckpoints = new Set();
let hlsInstance = null;
+ let activeLessonId = null;
document.addEventListener('DOMContentLoaded', async () => {
const token = localStorage.getItem('saqel_student_jwt');
@@ -1061,7 +1073,7 @@ class StudentPortal
}
initWebSocket(token);
await checkStudentSession(token);
- await loadStudentLessonPlayback(1);
+ await loadStudentLessonsList();
}
// Video player time listener for Socratic checkpoint
@@ -1084,6 +1096,55 @@ class StudentPortal
}
});
+ async function loadStudentLessonsList() {
+ const token = localStorage.getItem('saqel_student_jwt');
+ if (!token) return;
+ try {
+ const res = await fetch('/api/student/lessons', {
+ headers: { 'Authorization': 'Bearer ' + token }
+ });
+ const data = await res.json();
+ if (res.ok && data.data && data.data.length > 0) {
+ const lessons = data.data;
+ const countLabel = document.getElementById('lessons_count_label');
+ if (countLabel) countLabel.textContent = `${lessons.length} حصص متاحة`;
+
+ const carousel = document.getElementById('student_lessons_carousel');
+ if (carousel) {
+ carousel.innerHTML = lessons.map((les, idx) => `
+
+ `).join('');
+ }
+
+ if (!activeLessonId && lessons[0]) {
+ selectStudentLesson(lessons[0].id);
+ }
+ }
+ } catch (e) {
+ console.error('Load lessons list notice:', e);
+ }
+ }
+
+ function selectStudentLesson(lessonId) {
+ activeLessonId = lessonId;
+ const btns = document.querySelectorAll('#student_lessons_carousel button');
+ btns.forEach(b => {
+ b.style.background = 'rgba(255,255,255,0.06)';
+ b.style.color = 'var(--text-secondary)';
+ b.style.borderColor = 'var(--border)';
+ });
+ const activeBtn = document.getElementById(`btn_lesson_${lessonId}`);
+ if (activeBtn) {
+ activeBtn.style.background = 'linear-gradient(135deg, #0284C7, #0369A1)';
+ activeBtn.style.color = '#FFF';
+ activeBtn.style.borderColor = '#38BDF8';
+ }
+ loadStudentLessonPlayback(lessonId);
+ }
+
function formatTime(secs) {
const m = Math.floor(secs / 60).toString().padStart(2, '0');
const s = (secs % 60).toString().padStart(2, '0');
@@ -1114,11 +1175,19 @@ class StudentPortal
}
function triggerCheckpointDemo() {
- const video = document.getElementById('lesson_video_player');
- if (video) {
- video.currentTime = 14;
- triggeredCheckpoints.clear();
- video.play();
+ if (activeLessonCheckpoints.length > 0) {
+ renderSocraticModal(activeLessonCheckpoints[0]);
+ } else {
+ renderSocraticModal({
+ question_text: "إذا كان f(x) = sin(3x)، فما هي مشتقة الاقتران f'(x)؟",
+ options: [
+ { text: "3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)", is_correct: true },
+ { text: "cos(3x)", is_correct: false },
+ { text: "-3 cos(3x)", is_correct: false },
+ { text: "-cos(3x)", is_correct: false }
+ ],
+ rewind_on_fail_seconds: 45
+ });
}
}
@@ -1168,19 +1237,30 @@ class StudentPortal
}
}
- async function loadStudentLessonPlayback(lessonId = 1) {
+ async function loadStudentLessonPlayback(lessonId = 0) {
const token = localStorage.getItem('saqel_student_jwt');
if (!token) return;
try {
- const res = await fetch(`/api/lessons/${lessonId}/playback`, {
+ const targetUrl = lessonId > 0 ? `/api/lessons/${lessonId}/playback` : '/api/lessons/0/playback';
+ const res = await fetch(targetUrl, {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (res.ok && data.status === 'success') {
+ const les = data.data.lesson;
const pb = data.data.playback;
const checkpoints = data.data.checkpoints || [];
const chapters = data.data.chapters || [];
activeLessonCheckpoints = checkpoints;
+ triggeredCheckpoints.clear();
+
+ // Update Titles
+ if (les) {
+ const titleEl = document.getElementById('current_lesson_title');
+ const subtitleEl = document.getElementById('current_lesson_subtitle');
+ if (titleEl) titleEl.textContent = `الدرس: ${les.title}`;
+ if (subtitleEl) subtitleEl.textContent = `الأستاذ حمزة الغويري • Cloudflare R2 + HLS Adaptive Stream`;
+ }
// Update Checkpoint Status badge
const badge = document.getElementById('checkpoint_status_badge');
@@ -1190,16 +1270,28 @@ class StudentPortal
// Render AI Timeline Chapters
const chaptersList = document.getElementById('ai_timeline_chapters_list');
- if (chaptersList && chapters.length > 0) {
- chaptersList.innerHTML = chapters.map((ch, idx) => `
-
-
-
المحطة ${idx + 1}: ${escapeHtml(ch.title)}
-
${formatTime(ch.start_seconds)}
+ if (chaptersList) {
+ if (chapters.length > 0) {
+ chaptersList.innerHTML = chapters.map((ch, idx) => `
+
+
+ المحطة ${idx + 1}: ${escapeHtml(ch.title)}
+ ${formatTime(ch.start_seconds)}
+
+
${escapeHtml(ch.summary || '')}
-
${escapeHtml(ch.summary || '')}
-
- `).join('');
+ `).join('');
+ } else {
+ chaptersList.innerHTML = `
+
+
+ المحطة 1: الشرح الكامل والتطبيقات
+ 00:00
+
+
استعراض المفاهيم وتطبيقات المنهاج الوزاري.
+
+ `;
+ }
}
const video = document.getElementById('lesson_video_player');
diff --git a/backend/public/index.php b/backend/public/index.php
index 790d1f4..8d831ab 100644
--- a/backend/public/index.php
+++ b/backend/public/index.php
@@ -73,15 +73,13 @@ $router->get('/api/teacher/courses', [\App\Controllers\TeacherController:
$router->post('/api/teacher/courses', [\App\Controllers\TeacherController::class, 'addCourse'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/teacher/lessons', [\App\Controllers\TeacherController::class, 'addLesson'], [\App\Middlewares\AuthMiddleware::class]);
-// Dual Video Storage & Bunny.net Stream Routes (API-Driven)
+// Dual Video Storage & Cloudflare R2 / HLS Stream Routes (API-Driven)
$router->post('/api/teacher/videos/upload-direct', [\App\Controllers\VideoController::class, 'uploadDirect'], [\App\Middlewares\AuthMiddleware::class]);
-$router->post('/api/teacher/videos/bunny-create', [\App\Controllers\VideoController::class, 'createBunnyVideo'],[\App\Middlewares\AuthMiddleware::class]);
-$router->post('/api/teacher/videos/bunny-link', [\App\Controllers\VideoController::class, 'linkBunnyLesson'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/teacher/lessons/checkpoints', [\App\Controllers\VideoController::class, 'saveCheckpoint'], [\App\Middlewares\AuthMiddleware::class]);
+$router->get('/api/student/lessons', [\App\Controllers\VideoController::class, 'getStudentLessons'], [\App\Middlewares\AuthMiddleware::class]);
$router->get('/api/videos/stream/{uuid}', [\App\Controllers\VideoController::class, 'streamLocalVideo']);
$router->get('/api/videos/hls/{uuid}/{file}', [\App\Controllers\VideoController::class, 'streamHls']);
$router->get('/api/lessons/{id}/playback', [\App\Controllers\VideoController::class, 'getPlaybackData'], [\App\Middlewares\AuthMiddleware::class]);
-$router->post('/api/webhooks/bunny', [\App\Controllers\VideoController::class, 'handleBunnyWebhook']);
// Student & Teacher Chat Routes (API-Driven, Authenticated)
$router->get('/api/chat/conversations', [\App\Controllers\ChatController::class, 'getConversations'], [\App\Middlewares\AuthMiddleware::class]);