Skip to main content
Everything below is advisory or planned, not yet done. Items come from the “Suggestions”, “TO BE FIXED (later)” and clean-up-plan sections of 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 myExams readers only need quiz metadata — done 19/10/2026: they read the cards’ quiz_questions_count and attempts.
  • 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 user key of POST /auth/login itself (one round trip), and the theme paints from its cache. But the boot still waits for the whole GET /auth/user round trip behind LoadingProgressWidget (app.dart). Render the shell from the cached user (token + last user JSON in SharedPrefs) 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), make GET /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):
  1. GET /training/{id} ships every lesson’s full HTML content up front. Suggested: a structure endpoint (chapters/lessons without content) + per-lesson GET /lesson/{id} on demand, cached client-side.
  2. ✅ 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; the toggle_lesson_validation response did not need to change. Original suggestion: “Mark as complete & continue” waited on a full GET /user/{id} round-trip — return the toggled lesson id/state in the toggle_lesson_validation response and update currentUser.lessons locally.
Item 1 is still open: GET /training/{id} still loads chapters.lessons with their content.

One archive mode for the staff lists

Accept archive_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 pivot created_at to the user’s lessons (known issue 16).

Marketplace payload finishing touches

  • Add total_quizzes_count to /preview-trainings so the catalog cards can show the quizzes stat (known issue 3) — Training.quizzesCount already reads it.
  • Remove total_active_sales from MarketplacePreviewService::trainingCard / examCard if 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 migration 2026_04_17_000001_enforce_unique_certifications_per_target deletes the duplicate rows, then adds the certifications_student_training_unique and certifications_student_quiz_unique indexes, 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-php glyph 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 one NotificationEmail 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.
⚠️ The bundle ID is still the placeholder 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 the grep counts, the suggestion and the expected gain of each item, is on the Frontend Clean-Up page. The quick wins:
  • Trivial: silence debugPrint in release (598 calls, 487 in providers — one line in main(), plus dropping localStorage.debug = '*' from index.html); Sentry tracesSampleRate 1.0 → 0.1 (✅ moot since 19/10/2026: Sentry was removed from the app, Update 122); the 15 Opacity widgets → tinted colors / FadeTransition; RepaintBoundary around the painted banners and the DataGrid (3 in the whole app today); nginx Cache-Control headers (no-cache for index.html / main.dart.js, a week for canvaskit/* and fonts).
  • Low: drop the 59 UniqueKey() on routed pages (student_statefulshell.dart recreates a page on every shell rebuild — locale, theme or provider tick); memCacheWidth / cacheWidth on the 104 network and asset images (none set today); a ValueNotifier clock for the six 1-second timers that rebuild whole exam screens; FittedBox / ellipsis instead of the 78 AutoSizeText.
  • Medium: code splitting by role (above); first paint from the cached user (see the /auth/user item); ListView.builder for the 48 shrinkWrap lists; finer rebuilds (582 notifyListeners(), 92 context.watch, basic_info_screen.dart watching 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 of IntrinsicHeight rows in GlassCardGrid; self-hosting cropper.min.js (✅ gone: loaded on demand since 19/10/2026, Update 126, then removed with image_cropper on 28/10/2026, Update 166) and preloading Poppins.
✅ Applied so far: the review screens’ 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 (no FLAG_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 off key whose fr/ar strings are discount wording — switch to a dedicated key such as disabled (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 than isMobileDevice (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 hottest Selectors), auto_size_text → FittedBox / Text ellipsis in the quiz screens, then html_editor_enhanced → Fleather’s ParchmentHtmlCodec (the one that unlocks the win32 5 chain and five held packages), then expandable, modal_side_sheet, fluid_dialog, styled_divider whenever their screen is next touched. qr_flutter stays.
    • ✅ styled_divider removed 12/10/2026 (Update 100, the app’s own dashed divider).
    • ✅ html_editor_enhanced removed 14/10/2026 (Update 113): replaced by flutter_quill rather than Fleather, which ended the win32 5 chain; the web editor is SunEditor since 25/10/2026 (Update 153).
    • ✅ modal_side_sheet removed 28/10/2026 (Update 167, the app’s own BodyWithSideSheet).
    • 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.
Quick status (09/10/2026, D.2 updated 23/10/2026, re-checked 05/11/2026): frontend Sections B / C / D / D.2 / E / G.1 / G.2 are ✅ done (D.2, the 10 newly orphaned files, on 23/10/2026); frontend G.4 is 🔶 half done (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:
✅ Already done on the code side: Poppins trimmed from 18 faces to 6 (~2.1 MB freed), then to 4 Latin-subset faces on 19/10/2026 (Update 126), and 150 KB less font before the first frame on 31/10/2026 (Update 183). The deferred loading item (above) is step 3, after measuring.
(Also listed on the Frontend Clean-Up page.)