Implement verified curriculum, video review, watch sessions, ledger safeguards, and remove demo data

This commit is contained in:
Hamza-Ayed
2026-09-09 17:04:08 +03:00
parent 0dc5df7b5e
commit 5af684a892
49 changed files with 1809 additions and 3005 deletions
+109 -41
View File
@@ -21,8 +21,11 @@ namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Core\Database;
use App\Services\CurriculumService;
use App\Services\CurriculumExtractorService;
use App\Services\PublishedContentService;
use App\Services\LearningPackageService;
class CurriculumController
{
@@ -164,16 +167,33 @@ class CurriculumController
$response->json(['status' => 'success', 'log' => $logContent]);
}
/** GET /api/curriculum/lessons/{lessonId}/english-package */
public function getPublishedEnglishPackage(Request $request, Response $response): void
{
try {
$result=LearningPackageService::publishedEnglish(trim((string)$request->getParam('lessonId','')),(int)$request->user_id,$request->getHeader('x-national-id'));
$response->status((int)$result['http_status'])->json(array_diff_key($result,['http_status'=>true]));
} catch (\Throwable $e) { error_log('English package read failed: '.$e->getMessage()); $response->status(503)->json(['status'=>'unavailable','message'=>'تعذر قراءة حزمة الإنجليزية.']); }
}
/**
* Get Complete Live Tree
*/
public function getTree(Request $request, Response $response): void
{
$response->json([
'status' => 'success',
'data' => CurriculumService::getCurriculumTree()
]);
$tree = CurriculumService::getCurriculumTree();
try {
$published = Database::select("SELECT cl.uuid, cl.source_manifest_path, COUNT(vv.id) AS video_count FROM curriculum_lessons cl LEFT JOIN teacher_submissions ts ON ts.curriculum_lesson_id=cl.id AND ts.status='published' LEFT JOIN video_versions vv ON vv.id=ts.current_published_video_version_id AND vv.status='published' WHERE cl.source_status='approved' GROUP BY cl.id, cl.uuid, cl.source_manifest_path");
$byPath=[];
foreach ($published as $row) $byPath[(string)$row['source_manifest_path']]=['curriculum_lesson_id'=>$row['uuid'],'has_video'=>(int)$row['video_count']>0];
foreach ($tree as &$grade) foreach (($grade['subjects'] ?? []) as &$subject) foreach (($subject['semesters'] ?? []) as &$semester) foreach (($semester['units'] ?? []) as &$unit) foreach (($unit['lessons'] ?? []) as &$lesson) {
$path=(string)($lesson['file'] ?? '');
if (isset($byPath[$path])) $lesson=array_merge($lesson,$byPath[$path]);
}
unset($grade,$subject,$semester,$unit,$lesson);
} catch (\Throwable $e) { error_log('Published curriculum tree enrichment unavailable: '.$e->getMessage()); }
$response->json(['status'=>'success','data'=>$tree]);
}
/**
@@ -423,30 +443,18 @@ class CurriculumController
$type = $params['type'] ?? 'textbook';
$storage = realpath(__DIR__ . '/../../storage/curriculum');
$resolvedFile = null;
if (!empty($file) && !str_ends_with($file, '.pdf')) {
$cleanFile = ltrim($file, '/');
if (file_exists("{$storage}/{$cleanFile}")) {
$resolvedFile = "{$storage}/{$cleanFile}";
}
if (!$storage || empty($file) || !str_ends_with(strtolower($file), '.md')) {
$response->status(400)->json([
'status' => 'error',
'message' => 'يجب تحديد ملف Markdown معتمد للوثيقة المطلوبة.'
]);
return;
}
// If not resolved by exact path, resolve by subject
if (!$resolvedFile && !empty($subject)) {
if (str_contains($subject, 'english') || str_contains($subject, 'إنجليز')) {
$mds = glob("{$storage}/grade_10/english_10/semester_1/*/*.md");
if (!empty($mds)) $resolvedFile = $mds[0];
} elseif (str_contains($subject, 'physic') || str_contains($subject, 'فيزياء')) {
$mds = glob("{$storage}/grade_10/physics_10/semester_1/*/*.md");
if (!empty($mds)) $resolvedFile = $mds[1] ?? $mds[0];
} elseif (str_contains($subject, 'math') || str_contains($subject, 'رياضيات')) {
$mds = glob("{$storage}/grade_10/math_10/semester_1/*/*.md");
if (!empty($mds)) $resolvedFile = $mds[1] ?? $mds[0];
}
}
if ($resolvedFile && file_exists($resolvedFile)) {
$candidate = realpath($storage . '/' . ltrim($file, '/'));
$storagePrefix = rtrim($storage, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if ($candidate && str_starts_with($candidate, $storagePrefix) && is_file($candidate)) {
$resolvedFile = $candidate;
$content = file_get_contents($resolvedFile);
$response->json([
'status' => 'success',
@@ -458,26 +466,87 @@ class CurriculumController
return;
}
// Tailored subject fallback
$content = self::getDefaultSubjectDocument($subject, $type);
$response->json([
'status' => 'success',
'subject' => $subject,
'type' => $type,
'content' => $content
$response->status(404)->json([
'status' => 'error',
'message' => 'ملف الوثيقة المعتمد غير موجود أو غير متاح حالياً.'
]);
}
private static function getDefaultSubjectDocument(string $subject, string $type): string
/**
* Serves an approved asset from a published curriculum bundle by UUID only.
* GET /api/curriculum/assets/{assetId}
*/
public function getPublishedAsset(Request $request, Response $response): void
{
$s = strtolower($subject);
if (str_contains($s, 'english') || str_contains($s, 'إنجليز')) {
return "## Unit 01: Looking Good — Vocabulary & Reading\n\n### 1. Key Vocabulary & Word Formation\n- **Casual clothing:** Everyday informal garments (jeans, sneakers, hoodie).\n- **Traditional attire:** Cultural heritage wear (Jordanian Thobe and Keffiyeh).\n- **Subconscious influence:** The way external appearance affects cognitive confidence.\n\n### 2. Reading Text: The Power of First Impressions\nResearch shows that within the first seven seconds of meeting someone, people make subconscious judgments about character, competence, and reliability. In a famous experiment, doctors wearing clean white coats demonstrated higher diagnostic focus.\n\n### 3. Grammar Workshop: Articles (a, an, the, zero article)\n- Use **a / an** for non-specific singular countable nouns.\n- Use **the** when the listener knows which specific thing is meant.\n- Use **zero article** with plural or uncountable nouns spoken in general terms.";
$assetId = trim((string)$request->getParam('assetId', ''));
try {
$asset = PublishedContentService::findPublishedAsset($assetId);
} catch (\Throwable $e) {
error_log('Published asset lookup failed: ' . $e->getMessage());
$response->status(503)->json([
'status' => 'unavailable',
'message' => 'فهرس الأصول المنشورة غير متاح حالياً.'
]);
return;
}
if (str_contains($s, 'physic') || str_contains($s, 'فيزياء')) {
return "## الوحدة الأولى: المتجهات والكميات الفيزيائية\n\n### 1. التمييز بين الكميات القياسية والمتجهة\n- **الكميات القياسية:** تُحدد بالمقدار ووحدة القياس فقط (الكتلة، الزمن، الطاقة، درجة الحرارة).\n- **الكميات المتجهة:** تُحدد بالمقدار والاتجاه ونقطة التأثير (القوة، السرعة المتجهة، التسارع، الإزاحة).\n\n### 2. جمع وتحليل المتجهات بيانياً وتحليلياً\n- **المركبة الأفقية:** A_x = A cos θ\n- **المركبة الرأسية:** A_y = A sin θ\n- **المحصلة:** R = √(R_x² + R_y²)\n- **زاوية الاتجاه:** tan θ = |R_y / R_x|";
if (!$asset) {
$response->status(404)->json([
'status' => 'error',
'message' => 'الأصل المطلوب غير منشور أو غير متاح لك.'
]);
return;
}
return "## ملخص الوحدة والنتاجات التعليمية المقررة\n\n### 1. الأهداف العامة للمادة\n- استيعاب المفاهيم والمصطلحات الأساسية وفق المنهاج الوزاري المعتمد.\n- التدرب على حل النماذج والتطبيقات العملية والأسئلة الوزارية.\n- التحقق من اكتساب المهارات من خلال بنك الأسئلة التكيفي والمختبر.";
$path = PublishedContentService::readLocalAsset($asset);
if (!$path) {
$response->status(404)->json([
'status' => 'error',
'message' => 'النسخة المنشورة من الأصل غير متاحة في التخزين.'
]);
return;
}
if (str_starts_with((string)$asset['mime_type'], 'text/')) {
$content = file_get_contents($path);
if ($content === false) {
$response->status(503)->json(['status' => 'unavailable', 'message' => 'تعذر قراءة الأصل المنشور.']);
return;
}
$response->json([
'status' => 'success',
'asset' => self::assetMetadata($asset),
'content' => $content,
]);
return;
}
$response->setHeader('Content-Type', (string)$asset['mime_type']);
$response->setHeader('Content-Length', (string)$asset['byte_size']);
$response->setHeader('Content-Disposition', 'inline; filename="' . basename($path) . '"');
$response->sendHeaders();
readfile($path);
exit;
}
private static function assetMetadata(array $asset): array
{
return [
'asset_id' => $asset['uuid'],
'asset_type' => $asset['asset_type'],
'mime_type' => $asset['mime_type'],
'byte_size' => (int)$asset['byte_size'],
'sha256' => $asset['sha256'],
'bundle_version' => $asset['bundle_version'],
'curriculum_lesson_id' => $asset['curriculum_lesson_uuid'],
'identity' => [
'grade' => $asset['grade_key'],
'subject' => $asset['subject_key'],
'semester' => $asset['semester_key'],
'unit' => $asset['unit_key'],
'lesson' => $asset['lesson_key'],
],
];
}
/**
@@ -593,4 +662,3 @@ class CurriculumController
]);
}
}