From e4ec3f1ded693eb186fb7747eee5df39a5284ac7 Mon Sep 17 00:00:00 2001 From: a2nr Date: Wed, 17 Jun 2026 20:34:36 +0700 Subject: [PATCH] feat: custom evaluation config --- README.md | 7 + documentation.md | 1 + frontend/src/app.html | 9 ++ .../src/lib/components/CircuitEditor.svelte | 20 ++- frontend/src/lib/services/velxio-manager.ts | 49 +++++-- .../src/routes/lesson/[slug]/+page.svelte | 9 +- .../src/routes/lesson/[slug]/lesson.svelte.ts | 19 ++- frontend/static/favicon.ico | Bin 0 -> 7280 bytes frontend/static/favicon.png | Bin 0 -> 3969 bytes frontend/static/icons/icon-192.png | Bin 0 -> 14327 bytes frontend/static/icons/icon-512.png | Bin 0 -> 47271 bytes frontend/static/manifest.json | 49 ++++--- frontend/static/sw.js | 130 ++++++++++++++++++ services/lesson_service.py | 56 +++++++- 14 files changed, 305 insertions(+), 44 deletions(-) create mode 100644 frontend/static/favicon.ico create mode 100644 frontend/static/favicon.png create mode 100644 frontend/static/icons/icon-192.png create mode 100644 frontend/static/icons/icon-512.png create mode 100644 frontend/static/sw.js diff --git a/README.md b/README.md index c27a36c..cd53ea2 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ Untuk materi yang menggunakan simulator Arduino (Velxio): | `---EXPECTED_SERIAL_OUTPUT---` | Output serial yang diharapkan (subsequence match) | | `---EXPECTED_WIRING---` | Wiring yang harus dibuat siswa (JSON, lenient) | | `---KEY_TEXT---` | Kata kunci yang harus ada di kode siswa | +| `---EVALUATION_CONFIG---` | Konfigurasi tambahan evaluasi Arduino (JSON: e.g. `timeout_ms` dalam milidetik) | Contoh materi Arduino: @@ -246,6 +247,12 @@ LED OFF pinMode digitalWrite ---END_KEY_TEXT--- + +---EVALUATION_CONFIG--- +{ + "timeout_ms": 8000 +} +---END_EVALUATION_CONFIG--- ``` ##### Referensi Nama Pin Komponen Velxio diff --git a/documentation.md b/documentation.md index 573c094..853c27c 100644 --- a/documentation.md +++ b/documentation.md @@ -312,6 +312,7 @@ Elemes mendukung beberapa mode lesson melalui **marker** di file markdown. Mode | `---KEY_TEXT_CIRCUIT---` / `---END_KEY_TEXT_CIRCUIT---` | Keyword wajib di circuit (hybrid) | | `---SOLUTION_CODE---` / `---END_SOLUTION_CODE---` | Solusi kode (ditampilkan setelah selesai) | | `---SOLUTION_CIRCUIT---` / `---END_SOLUTION_CIRCUIT---` | Solusi circuit | +| `---EVALUATION_CONFIG---` / `---END_EVALUATION_CONFIG---` | Konfigurasi tambahan evaluasi Arduino (JSON: e.g. `timeout_ms`) | ### Fitur Tombol "Coba" (Code Try-out) diff --git a/frontend/src/app.html b/frontend/src/app.html index 0c79107..3dcf69e 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -14,5 +14,14 @@
%sveltekit.body%
+ diff --git a/frontend/src/lib/components/CircuitEditor.svelte b/frontend/src/lib/components/CircuitEditor.svelte index be9faf9..75b74c2 100644 --- a/frontend/src/lib/components/CircuitEditor.svelte +++ b/frontend/src/lib/components/CircuitEditor.svelte @@ -15,18 +15,21 @@ let ready = $state(false); let simApi = $state(null); let saving = $state(false); - let saveTimeout: ReturnType; - let autoSaveInterval: ReturnType; + let saveTimeout = $state | null>(null); + let isDestroyed = false; let lastLoadedCircuit = $state(''); let lastStorageKey = $state(undefined); function saveToStorage(text: string) { - if (!storageKey) return; + if (!storageKey || isDestroyed) return; saving = true; - clearTimeout(saveTimeout); + if (saveTimeout) clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { - localStorage.setItem(storageKey, text); + if (!isDestroyed && storageKey) { + localStorage.setItem(storageKey, text); + } saving = false; + saveTimeout = null; }, 1000); } @@ -137,8 +140,11 @@ // Cleanup timers on destroy $effect(() => { return () => { - clearInterval(autoSaveInterval); - clearTimeout(saveTimeout); + isDestroyed = true; + if (saveTimeout) { + clearTimeout(saveTimeout); + saveTimeout = null; + } }; }); diff --git a/frontend/src/lib/services/velxio-manager.ts b/frontend/src/lib/services/velxio-manager.ts index d4151e3..79d145e 100644 --- a/frontend/src/lib/services/velxio-manager.ts +++ b/frontend/src/lib/services/velxio-manager.ts @@ -44,9 +44,12 @@ export function initVelxioBridge( arduinoCircuitKey: string, arduinoCodeKey: string, onReady: (bridge: VelxioBridge) => void, - onSubmit: () => void -) { + onSubmit: () => void, + onCountdown?: (msRemaining: number) => void +): () => void { let settled = false; + let countdownInterval: ReturnType | null = null; + const onMessage = (e: MessageEvent) => { const type = e.data?.type; if (!type) return; @@ -83,8 +86,30 @@ export function initVelxioBridge( } if (type === 'velxio:compile_result' && e.data.success) { - const timeout = data?.evaluation_config?.timeout_ms ?? 5000; - setTimeout(() => onSubmit(), timeout); + const timeout = data?.evaluation_config?.timeout_ms ?? 8000; + if (countdownInterval) clearInterval(countdownInterval); + let remaining = timeout; + if (onCountdown) { + onCountdown(remaining); + countdownInterval = setInterval(() => { + remaining -= 1000; + if (remaining <= 0) { + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + } else { + onCountdown(remaining); + } + }, 1000); + } + setTimeout(() => { + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + onSubmit(); + }, timeout); } }; window.addEventListener('message', onMessage); @@ -136,10 +161,18 @@ export function initVelxioBridge( } catch { /* cross-origin or not ready yet */ } }, 1000); - setTimeout(() => { - clearInterval(pollReady); - if (settled) return; - settled = true; + const cleanup = () => { window.removeEventListener('message', onMessage); + clearInterval(pollReady); + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + }; + + setTimeout(() => { + cleanup(); }, 30_000); + + return cleanup; } diff --git a/frontend/src/routes/lesson/[slug]/+page.svelte b/frontend/src/routes/lesson/[slug]/+page.svelte index f6eb324..d16d484 100644 --- a/frontend/src/routes/lesson/[slug]/+page.svelte +++ b/frontend/src/routes/lesson/[slug]/+page.svelte @@ -101,7 +101,14 @@ beforeNavigate(() => { lessonContext.set(null); - if (mgr.velxioBridge) { mgr.velxioBridge.destroy(); mgr.velxioBridge = null; } + if (mgr.velxioCleanup) { + mgr.velxioCleanup(); + mgr.velxioCleanup = null; + } + if (mgr.velxioBridge) { + mgr.velxioBridge.destroy(); + mgr.velxioBridge = null; + } }); let contentEl = $state(null); diff --git a/frontend/src/routes/lesson/[slug]/lesson.svelte.ts b/frontend/src/routes/lesson/[slug]/lesson.svelte.ts index 423fee1..d6464e5 100644 --- a/frontend/src/routes/lesson/[slug]/lesson.svelte.ts +++ b/frontend/src/routes/lesson/[slug]/lesson.svelte.ts @@ -42,6 +42,7 @@ export class LessonManager { velxioSaving = $state(false); velxioError = $state(false); velxioIframe = $state(null); + velxioCleanup = $state<(() => void) | null>(null); showSolution = $state(false); activeTab = $state<'info' | 'exercise' | 'editor' | 'circuit' | 'output' | 'velxio' | 'flowchart'>('info'); @@ -177,6 +178,7 @@ export class LessonManager { this.circuitPassed = false; this.showSolution = false; + if (this.velxioCleanup) { this.velxioCleanup(); this.velxioCleanup = null; } if (this.velxioBridge) { this.velxioBridge.destroy(); this.velxioBridge = null; } this.velxioReady = false; this.velxioError = false; @@ -411,7 +413,11 @@ export class LessonManager { setupVelxioBridge(iframe: HTMLIFrameElement) { this.velxioIframe = iframe; - initVelxioBridge( + if (this.velxioCleanup) { + this.velxioCleanup(); + this.velxioCleanup = null; + } + this.velxioCleanup = initVelxioBridge( iframe, this.data, this.arduinoCircuitKey, @@ -420,7 +426,16 @@ export class LessonManager { this.velxioBridge = bridge; this.velxioReady = true; }, - () => this.handleVelxioSubmit() + () => this.handleVelxioSubmit(), + (msRemaining) => { + this.activeTab = 'output'; + Object.assign(this.velxioOut, { + loading: true, + output: `Kompilasi sukses. Menjalankan simulasi & merekam log serial (${Math.round(msRemaining / 1000)} detik)...`, + error: '', + success: null + }); + } ); } diff --git a/frontend/static/favicon.ico b/frontend/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5c579d4c8d8aae004dbdf90291b75cdb8b951ab2 GIT binary patch literal 7280 zcmeI%XEPjZzX0&Xt`=pL8_^}oifBs^qW89h)rq!xZy|cGL9j{?y@U{*=q0+Tq$T;m)h8WA(xleHV=F@}0Z@u&!Dj`&HBe0S?O=!L^w@ zlF&!lNel#FZG44Le1Wv|UN0gN3|=fvUda5FB|^ApDcbAB-R)gSI=h5a1PDMK>&_?< zkskfqfnD6_SuiI6!kjv9m^PC06B=21p%O+oKk2dl%y+_L`588Zg^GSZ`ZpTzV?={f zngTE*oLX*IM@hi~tv9jgv+?>wDDiNASk0=QVnW_e)%e@)Q>o}$*JblS%Dc_`T*n9~ zmEyDROmttn7u2r;m3wz0cH1(e1G?vA&ngRB4yN;ILV?O{9%)bz?_ z|4n9Mp&xC6!EJbZ*j3b8F?LNO7WQ)le1U+<(Pnjv8~$R+U!$n+(YReV4o3>mPPZMA zq}aLY*!SbwdGuxSx+|&0qSxs}pXc!t(#+-dV5tuTd4KX>tqUmpG(rvdL(}9sY4=`J z&-EnvV!<1&mPJP$8U`uS{7_>szM_BPd7j%}qS1EG(|Ub}gm28AXa&{(g(kH1O7zZub*5{TC-$YaU<(HmAcnqB z+wY~|78hBMLrL)ZxfP6y5g^DOQXR?h!)|zt) z8z99Bhzy6+Qr5~)s^A6I;#cA2$lyS5C3=~)i5z>_w8@hq(h5ku!xMfy`b2cu3-$vj z^#c8H%6bC??#Y75ie%s_%sNqxta!sD^T^MyFu%)qr3XKJBUqzs=$YVYmei|OO}p&-UI5p}Z0Gr@&w5?ioX#}ETdRVsU7$QAI?_EsF&a<=TqRX-SWKn zedc>D`^VYwWyPe^XZCL1blFibWj7hxVxH;KZGDUl#sx#?Pk%!y-wUxM5qL?5&qQ#GvhRYKVhj5b>zr1gx;Ve6^Ec-i3J)sMoK{1R^FVt@2o|9%`r+-orD>ST$#&Nm%q9p{R`3VEH(lEDf{CH`FEXpw8-*XK3bTqSig;xi;Wh@A&J zKWBnwW}!Mz8t58y5vm8>LMS;vvPBYQ)l1W-U^CGU`8Me`W{96QEn^}hWTU}vbdg>e z37I@drwoSFtH)ZI*f&}!s~oGGcPcp51vTK3TBWtnzJH=J<7eW};vWyl4Wz|m(=9lH zN-av8)MAkS>9$BcWGC{~=oONmyBl6uA@+g&1F|C5;H^$+rFf+$d<@S2UaEX-%51v= zLwsp-*_fT0-I6VHP+*+@t*R}_KkL%=)b>=FUV)S$5ts-~bWL>7)YA+Z_sLGo#x@Ze zix{gK2Q>LNO4;w(E8Dxxy3PidJuLq`H8SV$<<)!jX`dO*`RcjO;+VOHY4z!)n)w>? zJ(FFo9fO@!XeenFi5UJBDSn1U#sc3OUxgXeN=3w55CwG>W)v#Ae{tVM#8JrKrpkfQ zrDN>V(0NW=scNO_yAeEHZe5ny@iL||Hxy>v&{ogZ7sb7qx5+TRG;VG}XV`DTSo_II zwF%R*Y`f@kF*o`x-fa%?#HQMT$;r$`((0iLmA$UB*Eo&!o@4uT^XysLyL7X%71eg> z1JVQC{p4@bCY24s!44}k9t1uFWuIkZo-4OmSN+sVDuy}2G|G+3xhCi*oVPwtr1AOl zjqnYbzG%(>_Gq!({bjwIVbl=0VsCu4oAVY ziPKeuscxw9KXuJjo7h93e`Wt7=f>ey)`_g3s2F!RZ(L{;GUaw9#vVJS&nYL9U%|LD1VmG(9oG0I+ zPEj|nc>Q^uT(IIzJ~>|#t9^|^-0>ROT;;J!iHR2sT*n>9yP_1LrOuZ=4I`T~kQJ_ zw}wZ>?#g{hU6dbE@g(XbS|wt9pB_~5r#~L&Wm-MxZf|=1Lf`Prb+S@&evEh2>5LLf ze3qntuGfg3U2gGO%^WQmEqpXzx#?KCdoagx)_^5vc9)uh7n|zBPcFD7w0z0^nVhTV zKJrRON+Nx>9nAd9CMQLtMH%(QOuf(dC=OGmFMnK)k$xp5RZdUJ5bjG1AFQwz9J)+y zXK{0bI*Tu8IPr<&zzAW_?l{ge*V4MU+O-yQ9mT7C+S;PN=`0X8Z8pMMIGPfhm7B$KHy(zW%O=E z$D`Ao3?yq_?0W39x{t3JZ~3(E*|{z(+qXOTG&IfEw`>@UnVVczT^hR?tvaGx4$yRl z;_ksy4!_r3uuBGLfx~4+V-aBiMv*{Yc3T97@9MB2paT11b+S$RiWSYaX}R_=PI)6a zoq7JY3~O@oer~%}5L@@1i%pC9b;F?fb>7icX54i-vV0@0GL3W+mFEL{1v`~CzYSam zp$GQI!ll{o-UpXn?PBsLLe5~yFs+WwZkjIcV4l0PVTlCa6`?SpzPV!)#rQL4;I z?9eJ`9E({cy5`D57j};khjRWBoF{Je!EwhWa?dFhc;~44y^3ac%i>^SJVM&Nv-zj3Nihf&Q0`IOU1VAB7 z7(!zDZ3zItSyEMyh5O|OrKvmVT7skVFD6v2ia0sTbebCdcvPc`rqv29Kr5ekK@=sf z#Nh=zE1yp6ww2sB$YV2gnq1Y)>?5LOstv0|<%oKP1)DWJ2Nl2QbTbmzilQYZI|Kli zZW15g(_wZGa5}Frjku4Ls-DW2i6ghd`~@1|8}jCakuQBAV=}Deu8yKeR$ZS zLk|PqO`O}ZKG>IT&H1>-Uf~^nxajhkYj|`EBsvHfsEbqqAQJttc z9nuJ;Y!KUXU+OQkn!^O$NYuN zU&#E0%wNd-h0I^b{DsV4$oz%OU&#DFfDAD0zmVz7T=LS@qaF;oGEFU0oo#eT!=&Of z6oiBB7m%z2Mg=CsY`h1#m~LXe5aeIu<*rSy$ua}}Q3MmfAvCqZm|)GcKPSTD8wfWR zTt&Z;r|)*NpL#J*RJjE?9k?Ad(1dy(6Fv`7?sIL*yO7~Nt++f+kOe_LgOh3jrE612 zTwqnT-gzMVXHjR5otOB4irk{GwPjEw7+x7Koo0fpQ$f-0{dyU?krfJN^$r97cmf~!@>Y=vodQC%bqXsf0o(eEXC@WCKXuLUh&svapN0*(nGb?*d z3sjK&^kL2d9VKwICSWG-(&!y9rI~2ehI&_%Uym`Nf#3K|fz%!?h1dG{$>C+L_E5Q7 zwvi%1Uo#Lqb}5r^qI^P&OYf4>-E_}_LA$5lS~BdZ>b&0V2TAw!EHMeUy$rw68 zt*8k3@D7X)i_giq&=L_&k&sfWeE2AqPexA5H}!pUGo}e@z>{h*rC42N*ph4@i8d(sFaX5*e=c0Ffla zuyrqs%Eh;ti78{Rez#;Z8qhEl?_FQ(#`72$3~SNsik9Lv%z?_3xjah}CCeWIF9Qk9 zv{9;jyas;1T~b?FGP%pu_?H{~wH`6}Hds?cli@FTacbwg=>U!+j;Sbu=WXH@6reZ5v(cYG#hb|VlNjdj znRaJ!K5gw8bnJD7$wWK^7fD4nH~K#n3JmWp#4$GRgsbU|ZV)@YW$b=&5m9%gWqv$M z%EZCpDXIto1H*$a2`=fx7PQ=|5@@=~$Ldu$4Bo1tQv99`ETbr&jD4?Jpa?VIU6@u{ z&2uH|e)Z1D1MQWw~f3QVUb~` zO$|TRYZe(Z%;>x~GHv(~DOVv*!3Hc}6(6}Z%sq-z9n9nOg9?u5KzD$ib_pY1&{CZg zNk&x`yCuKaYm zTl+lU$tP^33WQt!-;ZD$Zi4r3wYR2@aOJsjT1IZ%fREKvRn@C1Sz9c-(dJDdfXf8R PpUk$$|Na*KFTno;)`!_} literal 0 HcmV?d00001 diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..a3e6168eaec12e3d7c649eaaa08392b36f5b7ca5 GIT binary patch literal 3969 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 literal 0 HcmV?d00001 diff --git a/frontend/static/icons/icon-512.png b/frontend/static/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..6b38f2e3d811856d8891e0b0ef4c17832560dfe8 GIT binary patch literal 47271 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 literal 0 HcmV?d00001 diff --git a/frontend/static/manifest.json b/frontend/static/manifest.json index 4a1ac43..f1de6a9 100644 --- a/frontend/static/manifest.json +++ b/frontend/static/manifest.json @@ -1,21 +1,32 @@ { - "name": "Elemes LMS", - "short_name": "Elemes", - "start_url": "/", - "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#0d6efd", - "description": "Belajar Pemrograman C", - "icons": [ - { - "src": "/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png" - } - ] + "name": "Elemes LMS", + "short_name": "Elemes", + "description": "Belajar Pemrograman C & Arduino", + "start_url": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#0a0a0a", + "theme_color": "#0d6efd", + "categories": ["education"], + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "shortcuts": [ + { + "name": "Daftar Pelajaran", + "url": "/", + "description": "Lihat semua pelajaran" + } + ] } diff --git a/frontend/static/sw.js b/frontend/static/sw.js new file mode 100644 index 0000000..2ae3d07 --- /dev/null +++ b/frontend/static/sw.js @@ -0,0 +1,130 @@ +// static/sw.js +const CACHE_VERSION = 'elemes-v1'; +const STATIC_CACHE = `${CACHE_VERSION}-static`; +const API_CACHE = `${CACHE_VERSION}-api`; +const ASSET_CACHE = `${CACHE_VERSION}-assets`; + +// Assets yang di-precache (Vite immutable + critical) +const PRECACHE_URLS = [ + '/', + '/manifest.json', +]; + +// Install: precache minimal assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(STATIC_CACHE).then(cache => cache.addAll(PRECACHE_URLS)) + .then(() => self.skipWaiting()) + ); +}); + +// Activate: hapus cache lama +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then(keys => + Promise.all(keys + .filter(k => k.startsWith('elemes-') && k !== STATIC_CACHE && k !== API_CACHE && k !== ASSET_CACHE) + .map(k => caches.delete(k)) + ) + ).then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Hanya handle GET + if (request.method !== 'GET') return; + + // NetworkOnly: API yang tidak boleh di-cache + if (url.pathname.startsWith('/api/compile') || + url.pathname.startsWith('/api/track-progress') || + url.pathname.startsWith('/api/login') || + url.pathname.startsWith('/api/logout')) { + return; // biarkan browser handle + } + + // CacheFirst: Static assets (Vite immutable dengan hash) + if (url.pathname.startsWith('/_app/immutable/')) { + event.respondWith(cacheFirst(request, STATIC_CACHE, 365)); + return; + } + + // CacheFirst: Gambar lesson (assets) + if (url.pathname.startsWith('/assets/')) { + event.respondWith(cacheFirst(request, ASSET_CACHE, 7)); + return; + } + + // CacheFirst: Font files (immutable) + if (url.hostname === 'fonts.gstatic.com') { + event.respondWith(cacheFirst(request, STATIC_CACHE, 365)); + return; + } + + // NetworkFirst: API lesson data (cache 6 jam) + if (url.pathname.startsWith('/api/lesson/') && url.pathname.endsWith('.json')) { + event.respondWith(networkFirst(request, API_CACHE, 6 * 60)); + return; + } + + // NetworkFirst: API lesson list (cache 1 jam) + if (url.pathname.startsWith('/api/lessons')) { + event.respondWith(networkFirst(request, API_CACHE, 60)); + return; + } + + // StaleWhileRevalidate: HTML pages + if (request.headers.get('accept')?.includes('text/html')) { + event.respondWith(staleWhileRevalidate(request, STATIC_CACHE)); + return; + } + + // StaleWhileRevalidate: Google Fonts CSS, KaTeX + if (url.hostname === 'fonts.googleapis.com' || url.hostname === 'cdn.jsdelivr.net') { + event.respondWith(staleWhileRevalidate(request, STATIC_CACHE)); + return; + } +}); + +// ── Helpers ────────────────────────────────────────────────────── + +async function cacheFirst(request, cacheName, maxAgeDays = 7) { + const cache = await caches.open(cacheName); + const cached = await cache.match(request); + if (cached) return cached; + const response = await fetch(request); + if (response.ok) cache.put(request, response.clone()); + return response; +} + +async function networkFirst(request, cacheName, maxAgeMinutes = 60) { + const cache = await caches.open(cacheName); + try { + const response = await fetch(request); + if (response.ok) cache.put(request, response.clone()); + return response; + } catch { + const cached = await cache.match(request); + if (cached) return cached; + return new Response(JSON.stringify({ error: 'Offline', cached: false }), { + status: 503, + headers: { 'Content-Type': 'application/json' } + }); + } +} + +async function staleWhileRevalidate(request, cacheName) { + const cache = await caches.open(cacheName); + const cached = await cache.match(request); + const networkFetch = fetch(request).then(response => { + if (response.ok) cache.put(request, response.clone()); + return response; + }).catch(() => null); + return cached || (await networkFetch) || + new Response('Offline — halaman ini belum pernah dibuka sebelumnya.', { + status: 503, + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }); +} diff --git a/services/lesson_service.py b/services/lesson_service.py index 967ebb0..2f59325 100644 --- a/services/lesson_service.py +++ b/services/lesson_service.py @@ -6,20 +6,42 @@ import os import re import html as html_module from functools import lru_cache +from threading import Lock import markdown as md from config import CONTENT_DIR +_home_cache = {'content': None, 'mtime': -1.0} +_home_lock = Lock() + +_markdown_cache = {} +_markdown_lock = Lock() + -@lru_cache(maxsize=1) def _read_home_md(): """Read home.md and return its content, or empty string if missing.""" path = os.path.join(CONTENT_DIR, "home.md") if not os.path.exists(path): return "" - with open(path, 'r', encoding='utf-8') as f: - return f.read() + try: + current_mtime = os.path.getmtime(path) + except OSError: + return _home_cache.get('content') or "" + + with _home_lock: + if current_mtime != _home_cache['mtime']: + with open(path, 'r', encoding='utf-8') as f: + _home_cache['content'] = f.read() + _home_cache['mtime'] = current_mtime + # Invalidate all downstream caches + find_lesson_file.cache_clear() + get_lessons.cache_clear() + get_lesson_names.cache_clear() + get_lessons_with_learning_objectives.cache_clear() + with _markdown_lock: + _markdown_cache.clear() + return _home_cache['content'] def _parse_lesson_links(home_content): @@ -417,9 +439,18 @@ def _extract_section(content, start_marker, end_marker): return extracted, remaining -@lru_cache(maxsize=32) def render_markdown_content(file_path): """Parse a lesson markdown file and return structured HTML parts as a dictionary.""" + try: + current_mtime = os.path.getmtime(file_path) + except OSError: + current_mtime = 0.0 + + with _markdown_lock: + cached = _markdown_cache.get(file_path) + if cached and cached['mtime'] == current_mtime: + return cached['data'] + with open(file_path, 'r', encoding='utf-8') as f: content = f.read() @@ -586,7 +617,7 @@ def render_markdown_content(file_path): exercise_html = md.markdown(exercise_content, extensions=MD_EXTENSIONS) if exercise_content else "" lesson_info_html = md.markdown(lesson_info, extensions=MD_EXTENSIONS) if lesson_info else "" - return { + parsed_data = { 'lesson_html': lesson_html, 'exercise_html': exercise_html, 'expected_output': expected_output, @@ -615,10 +646,21 @@ def render_markdown_content(file_path): 'slides': slides_html } + with _markdown_lock: + if len(_markdown_cache) >= 128: + _markdown_cache.clear() + _markdown_cache[file_path] = {'data': parsed_data, 'mtime': current_mtime} + + return parsed_data + -@lru_cache(maxsize=1) def render_home_content(): - """Render the home.md intro section (before Available_Lessons) as HTML.""" + """Render the home.md intro section (before Available_Lessons) as HTML. + + Not cached separately — relies on _read_home_md() mtime-based cache, + which is already fast (1 syscall per request). This avoids the lru_cache + multi-worker stale data problem. + """ home_content = _read_home_md() if not home_content: return ""