LMS_Frontend/major_code_updates.md and
LMS_Frontend/clean_up.md. Bugs that must be fixed live in
Issues & Errors instead.
✅ marks sub-items already shipped (kept here for context), ⚠️ marks items with a caveat to read
before acting, 🔥 marks the highest-value items.Last checked against the code of
LMS_Backend and LMS_Frontend (chat+ai branches) on
05/11/2026: every item without a ✅ is still open.🔥 Backend — high-value changes
✅ Slim down the /auth/user login payload
✅ Done 19/10/2026 (backend + app, Update 125). GET /auth/user is now built field by field by
AuthUserPayloadService (about 1.5 MB → 20 KB): profile, company, role permissions, the newest
notifications and, for a student, the my_trainings[] / my_exams[] cards with a progress /
attempts summary the server computes. No exam question, option or attempt ships in it any more,
so the QuizResource whitelist that dropped tip / quiz_question_type_id no longer matters
(?legacy=1 returned the old payload for one release). Kept here for context — see
Authentication and Auth User Optimize. Only
the frontend’s first-paint part (last bullet) is still open.
Original suggestion (Update 35 red flag): the student’s whole “my exams” dataset — every question,
every option with is_correct, every attempt — shipped inside the login response and was cached
for the session. It was huge, stale by design, and its QuizResource whitelist silently dropped
columns.
- ✅ Strip exam
quiz_questions(and ideally attempts) out of/auth/user— done 19/10/2026; only the attempt summary (used / allowed / best score / passed) is left. - ✅ Consumers that need questions should fetch by id — done 19/10/2026: the exam review loads the
attempt with its answers, then
GET /quiz/{id}. - ✅ The remaining
myExamsreaders only need quiz metadata — done 19/10/2026: they read the cards’quiz_questions_countandattempts. - Don’t add more data to the login payload.
- 🔥 Frontend half (App Speed item #2, 08/10/2026) — still open on a reload or a return visit.
✅ 19/10/2026 (Update 126): a sign-in builds the dashboard from the
userkey ofPOST /auth/loginitself (one round trip), and the theme paints from its cache. But the boot still waits for the wholeGET /auth/userround trip behindLoadingProgressWidget(app.dart). Render the shell from the cached user (token + last user JSON inSharedPrefs) and refresh in the background; My Trainings / My Exams / Attempts then load with their own calls when the tab opens (they already have refresh methods).
Harden the full-payload endpoints 🔥
Now that no student screen calls them (Update 44), makeGET /training/{id}
(TrainingPolicy::view is return true) and GET /quiz/{id} (students hold QUIZZES_VIEW by
default) staff / owner-only — otherwise anyone who types the URL still gets lesson content and
answer keys. Tracked as a critical item in Issues & Errors.
✅ Fix GET /training/{id} chapter-quiz serialization, then delete the frontend backfill
✅ Done 19/10/2026 (backend), together with multiple quizzes per chapter. Every chapter
payload (GET /training/{id}, GET /chapter(s), the login payload, the marketplace preview
outline and the chapter exports) now carries a quizzes array — all the chapter’s quizzes, in
creation order, whether or not the chapter has lessons — plus the deprecated quiz (= the first
one) for older app builds. Students get the published quizzes, staff also get the drafts. A chapter
can hold any number of quizzes: creating a second one no longer turns the first into an exam (only
the training’s final quiz stays one per training). Quiz and exam slugs are now generated by the
backend too. No lesson-dependent drop could be reproduced: the “missing” quiz was most likely a
draft, which GET /chapter/{id} (the app’s backfill) returned and GET /training/{id} never
did. Kept here for context — see
Chapters and
Quizzes & Exams (v4).
Original suggestion (Update 34): the endpoint omits a chapter’s quiz when the chapter has no
lessons. Load chapters.quiz.* as a first-class sibling of chapters.lessons, then remove the
GET /chapter/{id} backfill in TrainingQuizProvider.getTrainingWithQuizAttempts().
Lightweight course-player loading
Two measured costs in the player (Update 37):GET /training/{id}ships every lesson’s full HTML content up front. Suggested: a structure endpoint (chapters/lessons withoutcontent) + per-lessonGET /lesson/{id}on demand, cached client-side.- ✅ Done 19/10/2026 (app side, Update 125). After “Mark as complete & continue” the app now
updates the validated lesson ids locally and re-syncs the card’s progress, so the
GET /user/{id}round trip is gone; thetoggle_lesson_validationresponse did not need to change. Original suggestion: “Mark as complete & continue” waited on a fullGET /user/{id}round-trip — return the toggled lesson id/state in thetoggle_lesson_validationresponse and updatecurrentUser.lessonslocally.
GET /training/{id} still loads chapters.lessons with their content.
One archive mode for the staff lists
Acceptarchive_status=active|archived|all (or with_archived=1) on the list endpoints and return
archived_at in every row. The frontend’s “All records” view (Update 85) currently merges two
paginated lists at 2–3 requests per page — it would then send one request and drop the merge
(known issue 31).
Gamification: weekly window + levels
GET /progress/leaderboard / /progress/score are all-time only and carry no level. Add a weekly
score (for example ?period=week) and a level / tier per entry so the leaderboard card can render
the template’s “This week” pill and “Level 42 – Diamond” lines (known issue 13).
Lesson validation timestamps
A validated lesson carries no completion date, so the “Trainings in progress” card cannot show ”· 2 touched this week”. Add the pivotcreated_at to the user’s lessons (known issue 16).
Marketplace payload finishing touches
- Add
total_quizzes_countto/preview-trainingsso the catalog cards can show the quizzes stat (known issue 3) —Training.quizzesCountalready reads it. - Remove
total_active_salesfromMarketplacePreviewService::trainingCard/examCardif sales figures should not reach students’ network tabs at all (Update 47).
Certifications: defense in depth
- ✅ DB-level unique constraints on
(student_id, training_id)and(student_id, quiz_id)(Update 3’s recommendation) — already in the backend, checked 05/11/2026: the migration2026_04_17_000001_enforce_unique_certifications_per_targetdeletes the duplicate rows, then adds thecertifications_student_training_uniqueandcertifications_student_quiz_uniqueindexes, so concurrent requests can no longer create a second certificate. - Server-side ownership + completion checks in
CertificationsController::store— this one is a critical issue, tracked in Issues & Errors (#1). - Arabic certificate share images: bundle an Arabic font and shape the text (e.g. the
ar-phpglyph helper) or render that case with a headless browser (known issue 30).
Notification e-mails: an archive copy (BCC) — Update 94 question
E-mails sent from the admin Reminders page never appear in a “Sent” folder: the backend queues oneNotificationEmail per recipient (NotificationService, Mail::to(...)->queue) through the
platform’s SMTP account, not the admin’s mailbox, and SMTP keeps no copies — the platform’s own
record is the Recent log. Without code, an admin can tick their own account (or a team account that
is a platform user) as an extra recipient: each recipient gets a separate mail, so learners never
see that address; it counts toward the 10-recipient e-mail limit and adds an in-app notification
for that account. If wanted on the backend: a BCC archive address — for example a
NOTIFICATIONS_ARCHIVE_BCC setting added with ->bcc(...) in NotificationEmail::build(), or a
per-company field next to the SMTP settings. BCC, never CC (CC shows the address to every learner).
⚠️ The “Platform access” template carries a password, so any archive mailbox would hold it too.
AI assistant: move the per-training toggle to the backend
Update 38 shipped it with SharedPreferences persistence by design. The migration plan:trainings.ai_assistant_enabled boolean column (default true), nullable|boolean validation with a
request()->has() guard, field in $fillable + resources; frontend then reads the payload field,
drops ai_assistant_local_settings.dart, and the field gets documented in
api-reference/trainings.mdx.
🔥 Frontend — aspirations & deferred features
Boot-failure retry button (app.dart)
On a boot-time network failure, authResultNotifier never leaves null and the app sits on a
spinner / blank screen forever. Distinguish “loading” from “failed” and render an error view with a
Retry button that re-runs _fetchAuthData() (keys noInternetError / anErrorHasOccurred
already exist in all three locales).
Real device push notifications (FCM) — investigated, deferred by owner
In-app notifications are pull-based today; nothing reaches a closed device. The full turn-key checklist is written (Update 35-era suggestion block):1
External setup (owner)
Create a Firebase project (Android + iOS apps,
flutterfire configure); for iOS a paid Apple
Developer account + APNs Auth Key (.p8) uploaded to Firebase.2
Flutter app
firebase_core + firebase_messaging, a PushNotificationService (token → backend after
login, cleared on logout), the three message states wired (foreground via the existing
flutter_local_notifications, tap deep-links via go_router, background handler).3
Native shells
Android google-services plugin; iOS Push capability + Background Modes + AppDelegate bridge.
4
Backend
A register-device-token endpoint and an FCM fan-out wherever a notification is created — without
this send-side, no push ever arrives.
com.example.strived; pick the real reverse-domain ID
before registering Firebase. ⚠️ Don’t ship the app side half-wired and assume users get pushes.
Shrink main.dart.js with deferred loading 🔥
Measured 08/10/2026 on the release build (Flutter 3.44.6, CanvasKit): main.dart.js is 12.2 MB
(3.2 MB gzipped) and deferred as is used 0 times, so every visitor — a student included —
downloads the admin / super-admin / instructor / content-manager pages (Syncfusion DataGrid,
fl_chart, Quill / Fleather, html_editor_enhanced, the PDF viewer, the image cropper). Import
each role shell deferred as and await loadLibrary() in its GoRouter redirect / branch (Flutter
web emits one .part.js per deferred library); do the same for the single-screen heavy libraries.
The 08/10 dead-code deletion did not change the bundle size — dead code was already
tree-shaken; only code splitting shrinks it. Item #1 of the App Speed table below.
App speed suggestions — 16 measured items (08/10/2026)
The full table, with thegrep counts, the suggestion and the expected gain of each item, is on
the Frontend Clean-Up page. The quick wins:
- Trivial: silence
debugPrintin release (598 calls, 487 in providers — one line inmain(), plus droppinglocalStorage.debug = '*'fromindex.html);Sentry(✅ moot since 19/10/2026: Sentry was removed from the app, Update 122); the 15tracesSampleRate1.0 → 0.1Opacitywidgets → tinted colors /FadeTransition;RepaintBoundaryaround the painted banners and the DataGrid (3 in the whole app today); nginxCache-Controlheaders (no-cacheforindex.html/main.dart.js, a week forcanvaskit/*and fonts). - Low: drop the 59
UniqueKey()on routed pages (student_statefulshell.dartrecreates a page on every shell rebuild — locale, theme or provider tick);memCacheWidth/cacheWidthon the 104 network and asset images (none set today); aValueNotifierclock for the six 1-second timers that rebuild whole exam screens;FittedBox/ ellipsis instead of the 78AutoSizeText. - Medium: code splitting by role (above); first paint from the cached user (see the
/auth/useritem);ListView.builderfor the 48shrinkWraplists; finer rebuilds (582notifyListeners(), 92context.watch,basic_info_screen.dartwatching one provider 12 times);heroicons(1,288 bundled SVGs, 5.1 MB, 71 used) → Material icons or 71 local SVGs; fixed card heights per breakpoint instead ofIntrinsicHeightrows inGlassCardGrid;self-hosting(✅ gone: loaded on demand since 19/10/2026, Update 126, then removed withcropper.min.jsimage_cropperon 28/10/2026, Update 166) and preloading Poppins.
FlipCard became a static swap (ExplanationSwap) and the
exam attempt review page lays out from 960px with the StudentWebLayout.review* sizes. Next: the
training review twins (training_quiz_review_*) onto the same tokens.
✅ Also applied since: on 19/10/2026 (Update 126) a sign-in builds the dashboard in one round trip,
the theme paints from its cache, Poppins ships as 4 Latin-subset faces (about 26 KB each instead of
155 KB) and Cropper.js left index.html; on 31/10/2026 (Update 183) the web app loads 150 KB less
font before its first frame. Still at 0: deferred as imports.
Optional screenshot blocking on sensitive screens
Audited: screen capture is allowed everywhere (noFLAG_SECURE, no blocking package). If ever
wanted, decide app-wide vs per-screen first, then FLAG_SECURE on Android + a secure overlay on
iOS, or a package like no_screenshot.
✅ Marketplace tab badges: prefetch the non-active catalog
✅ Done 21/10/2026 (Updates 138–139), without an extra request. Page 1 of/preview-trainings, /preview-exams and /instructors carries the backend’s cached
catalog_counts, so every Marketplace tab badge shows on first landing, whichever tab is open
(known issue 44, resolved). Kept here for context.
Original suggestion (Update 9): on first landing in Marketplace, fetch the other catalog’s first
page (or a lightweight count) so both tab badges show immediately — trigger from
addPostFrameCallback, only when the total is still null.
Voucher success → proper in-app refresh
The “Go to My Content” button after applying a voucher does a full page reload on web, because the earlier in-place refresh surfaced unowned trainings and was removed. The real fix:EnrolledTrainings / EnrolledQuizzes freeze their lists in initState — a genuine refresh needs
the Selector3 to rebuild with fresh references while My Content is mounted, after the
“shows unowned trainings” bug is understood. ⚠️ The mobile path still needs its own verify pass.
Make the Swift Package Manager “off” switch repo-enforced
SPM off is a machine-local override (~/.config/flutter/settings) while Flutter 3.44 defaults
it on; a fresh machine or flutter config --clear-features reintroduces the Update 28 Sentry hang.
Options, in value order: (1) a preflight check that hard-errors when the flag is not false,
(2) document the command as mandatory per-machine setup, (3) re-test whether a newer
sentry_flutter removes the hang entirely. ⚠️ ~/.flutter is the analytics file, not the feature
flags; flutter_keyboard_visibility is transitive and can’t be bumped from pubspec.yaml.
Status 05/11/2026: ✅ option (2) is done — the README’s iOS setup lists
flutter config --no-enable-swift-package-manager next to brew install cocoapods. ✅ The cause
behind option (3) is gone: sentry_flutter left the app on 19/10/2026 (Update 122) and its pods
are out of ios/Podfile.lock, so the Sentry SPM download that hung no longer happens. Nobody has
tested turning SPM back on yet, and flutter_inappwebview_ios (through cloudflare_turnstile) still
has no SPM support (known issue 60). Option (1), the preflight check, is still open.
Smaller polish items
- Narrow the lesson column from 960px toward the template’s ~720–800px for better line length — weigh against shrinking lesson images/PDFs (Update 40).
- Web course-player header: the “Off” pill reuses the
offkey whose fr/ar strings are discount wording — switch to a dedicated key such asdisabled(Update 51 note). - Dashboard enrolled-card meta row overflows a few px on narrow cards — shrink the avatar below a
card-width threshold; don’t hide it with
FittedBox. ⚠️ The parity test deliberately drains these exceptions today; remove the drains when fixing. - Below 600px a desktop browser shows undersized cards: branch the width on
ResponsiveWidget.isSmallScreen(viewport) rather thanisMobileDevice(device). - Stale-package replacements, in the suggested order (clean-up table G.4, 08/10/2026):
tuple→ Dart 3 records (40 files, mechanical, touches the hottestSelectors),auto_size_text→FittedBox/Textellipsis in the quiz screens, thenhtml_editor_enhanced→ Fleather’sParchmentHtmlCodec(the one that unlocks thewin325 chain and five held packages), thenexpandable,modal_side_sheet,fluid_dialog,styled_dividerwhenever their screen is next touched.qr_flutterstays.- ✅
styled_dividerremoved 12/10/2026 (Update 100, the app’s own dashed divider). - ✅
html_editor_enhancedremoved 14/10/2026 (Update 113): replaced byflutter_quillrather than Fleather, which ended thewin325 chain; the web editor is SunEditor since 25/10/2026 (Update 153). - ✅
modal_side_sheetremoved 28/10/2026 (Update 167, the app’s ownBodyWithSideSheet). - Still to do:
tuple,auto_size_text,expandable,fluid_dialog.
- ✅
- ✅ Done 01/11/2026 (Update 188). The Exam Catalog and the Instructors tab shared one Filters
side-sheet flag (
ExamProvider.showSideSheetExam, Update 90 note). The flag is gone: each Marketplace page opens its own filter drawer over the catalog, and since Update 190 (same day) the Instructors tab has only a search bar.
🧹 Repository clean-up & package upgrades → the Clean Up tab
The full clean-up plans now live in their own tab, with every table and the complete installed-vs-latest package versions:Frontend Clean-Up (Flutter)
graphify-out untracking (~47 MB, still pending); the dead code (56 files + 11 assets), the 13
unused packages and the Flutter 3.44.6 package bump — all ✅ done 08/10/2026, plus 27 more
orphaned files ✅ deleted 23/10/2026; the full
installed-vs-latest table with why 21 packages are held, the G.4 stale-package replacement
plan, the asset audit and the 16 App Speed Suggestions.
Backend Clean-Up (Laravel)
graphify-out untracking (~12 MB), the duplicated abandoned CORS packages (✅ removed
19/10/2026), unused jwt-auth / geoip, the composer version table (✅ Laravel 12 since
25/10/2026, was Laravel 10 EOL 🔴), ✅ one PDF engine (wkhtmltopdf / Snappy removed
24/10/2026), Pint debt, and the backend optimization backlog.
flip_card 08/10, styled_divider 12/10,
html_editor_enhanced 14/10 and modal_side_sheet 28/10/2026 replaced; tuple,
auto_size_text, expandable and fluid_dialog left); frontend A (graphify-out,
.DS_Stores, stray files) and F are ⬜ pending. Backend: ✅ the duplicated CORS packages
removed 19/10/2026 (Update 126), ✅ one PDF engine (dompdf) 24/10/2026, ✅ Laravel 10 → 12.70.1
25/10/2026 (Sanctum 4, PHPUnit 11); the rest of the backend plan (graphify-out, jwt-auth / geoip,
images, Pint, lang folders, the Telescope tables) is ⬜ pending.
⚡ Web performance — the nginx fix (compression live, caching not yet applied) 🔶
Update 33’s cold-load diagnosis:canvaskit.wasm ships 7.2 MB uncompressed and nothing carries
a Cache-Control header, so every repeat visit re-validates ~10 MB. The fix is Plesk → nginx
directives on each vhost (no rebuild needed): enable gzip/brotli for application/wasm (and json /
fonts), mark the content-hashed assets public, max-age=31536000, immutable, and keep
index.html + flutter_service_worker.js at no-cache.
Status 05/11/2026 (checked on elearning.theteam.com.tn with the command below):
✅ compression is live — canvaskit.wasm, main.dart.js and index.html come back with
content-encoding: gzip. Still open: none of them carries a Cache-Control header, so repeat
visits still re-validate everything. (The service-worker line no longer applies: the web app has no
service worker since Update 127.) Verify with: