LMS_Frontend/major_code_updates.md
(last reviewed: 07/10/2026 05 PM). This page groups those rows — plus the open “TO BE FIXED”
items from the update entries — by priority. The #N markers are the row numbers in that
table, so the two stay easy to cross-reference.
This page was re-checked on 09/10/2026 against the 08/10/2026 clean-up (dead code deleted,
every package bumped) and Updates 90–94, and on 19/10/2026 for Update 123 (rows #34 and #35 added,
the chapter-quiz backfill closed) and Update 124 (row #36 added), and on 20/10/2026 for Update 129 (rows #40–#42 added; one more row is tracked privately in the frontend log), and on 01/11/2026 for Update 184 (rows #50 and #51, the R2 bucket folders, closed: see Recently resolved) and Update 185 (row #5 narrowed, row #55 added), and on 04/11/2026 for Updates 223–224 (row #7 narrowed, row #63 added; one more row is tracked privately in the frontend log). Notes marked 09/10 record what changed since the table
was last reviewed; the table itself has not been updated yet.
🔴 Critical — security & data exposure
#1 — POST /certification trusts the client (Backend)
#1 — POST /certification trusts the client (Backend)
student_id is the caller nor that the training / exam was
completed (CertificationPolicy::create returns true for everyone). The “finished” gate
exists only in the Flutter UI — a logged-in user can issue a certification for an
unfinished course, or for another user, by calling the API directly.
Fix: server-side ownership + completion check in CertificationsController::store.#17 — POST /notifications/send has no role check (Backend)
#17 — POST /notifications/send has no role check (Backend)
SendUnifiedNotificationRequest::authorize() returns true and the controller checks no role:
any signed-in user, a student included, can notify any user, role, or the whole company.
Fix: allow admins / instructors only, and limit an instructor to the learners of their own
classrooms.#21 — Classroom write endpoints have no role check (Backend)
#21 — Classroom write endpoints have no role check (Backend)
POST /classroom/create, PUT /classroom/update/{id}, archive / unarchive and
DELETE /classroom/{id}/force_delete check no role (a student creating a classroom answers
201). update and force_delete also skip the company filter, so another company’s
classroom can be changed or deleted. Update 73 only fixed the read endpoints.
Fix: staff only (instructor limited to own classrooms) + where('company_id', …).Full-payload endpoints still open to any student (Backend — Update 44 follow-up)
Full-payload endpoints still open to any student (Backend — Update 44 follow-up)
GET /training/{id} (TrainingPolicy::view is return true) still returns every lesson’s
content and every quiz answer key, and GET /quiz/{id} is reachable because students hold
QUIZZES_VIEW by default. The marketplace no longer calls them (Update 44), but anyone who
types the URL still gets the data. Fix: make both staff / owner-only now that no student
screen needs them.⚠️ Unverified — GET /quiz/{id} may ship the answer key before an exam starts
⚠️ Unverified — GET /quiz/{id} may ship the answer key before an exam starts
authed_user_and_attempts.md) and
not yet re-checked against the code. Trace the endpoint through the providers and screens;
if confirmed, promote it into the table above.🚩 High — wrong behavior users can hit
#7 — companyId: 1 hardcoded in student flows (Frontend)
#7 — companyId: 1 hardcoded in student flows (Frontend)
StudentProvider.fetchPaginatedTrainings, the createCertificateApi call sites, their
notification refresh and QuizProvider.uploadFile() all send company_id: 1 — wrong for any
tenant other than company 1. Fix: read the company from the signed-in user (and grep for
the remaining hardcoded instances — they’re latent multi-tenant bugs).
04/11: the notifications page now sends the signed-in user’s company_id (Update 223); the
other call sites above are unchanged.#20 — Dashboard 'Edit profile information' opens the shopping cart (Frontend)
#20 — Dashboard 'Edit profile information' opens the shopping cart (Frontend)
profile_side_widget.dart calls studentNavigationShell.goBranch(5) — the cart branch,
not Settings (6). Fix: navigate like the app-bar avatar does (setSelectedIndex(6) +
goBranch(6) + the settings sub-branch).
09/10: the file was deleted on 08/10/2026 by the dead-code clean-up (Section D — nothing
imported it), so the button no longer exists anywhere in the app. The source table still
carries the row; it can be closed.#29 — Archived trainings stay visible to their owners (Frontend + Backend)
#29 — Archived trainings stay visible to their owners (Frontend + Backend)
myTrainings is built at login from orders[].trainings, whose relation deliberately drops
the archived scope — so an archived training still shows on My Trainings, the dashboard cards
and the overview counts (Update 82 only fixed the Certificates “In progress” list), and the two
data sources disagree until a refetch. Fix: decide the product rule, then filter
archivedAt == null where myTrainings is built — or scope the login resource.#2 — Certificate created but the API answers 500 (Backend)
#2 — Certificate created but the API answers 500 (Backend)
already_exists: true). Fix: guard the notification (try/catch or queue).#22 — An archived classroom cannot be deleted (Backend)
#22 — An archived classroom cannot be deleted (Backend)
DELETE /classroom/{id}/force_delete hides archived classrooms in its lookup, so it answers
404 — yet the admin table only offers Delete on archived classrooms. Fix: look up
with withArchived() (plus the #21 company filter).#23 — Endless spinner when a classroom request fails (Frontend)
#23 — Endless spinner when a classroom request fails (Frontend)
GET /classroom/{id} fails (403 for a non-member, 404, network),
getClassroomById shows the error dialog and returns null — the page keeps its loading spinner
forever behind it. Fix: an error state with a back button.#31 — No backend 'all records' mode for the staff lists (Backend)
#31 — No backend 'all records' mode for the staff lists (Backend)
Archivable scope cannot return active + archived together, so the frontend builds the
“All” view from two paginated lists: 2–3 requests per page and active rows always sorted
before archived ones. Fix: accept archive_status=active|archived|all and return
archived_at in every row (see Suggested Changes).⚠️ Medium — gaps and performance costs with workarounds
#3 — /preview-trainings has no total_quizzes_count (Backend)
#3 — /preview-trainings has no total_quizzes_count (Backend)
total_quizzes_count (chapter quizzes + final quiz) lights it up with no Flutter change.#5 — My Content tab bodies are rebuilt with UniqueKey (Frontend)
#5 — My Content tab bodies are rebuilt with UniqueKey (Frontend)
CertificatesScreen refetches
/certifications/by-user and the My Trainings search field loses its text. Fix: stable
keys (or none).01/11/2026 (Update 185): My Trainings and My Exam Simulator now have stable keys (their search,
category filter and groups survive rebuilds). Only Certificates and Exam Attempts still use
UniqueKey().#12 — Three HTML renderers still bypass AppHtmlContent (Frontend)
#12 — Three HTML renderers still bypass AppHtmlContent (Frontend)
question_explanation_view.dart) and the admin add-training preview render HTML outside the
shared renderer — a YouTube iframe inside a question explanation would show “Error 153” on
mobile. LessonContentScreen is a fourth, unused copy. Fix: migrate them; delete
LessonContentScreen.
09/10: LessonContentScreen was deleted on 08/10/2026 (dead-code Section D); the three live
renderers still need the migration.#13 — Leaderboard score is all-time only, no levels (Backend)
#13 — Leaderboard score is all-time only, no levels (Backend)
#16 — Validated lessons carry no completion date (Backend)
#16 — Validated lessons carry no completion date (Backend)
created_at to the user’s lessons.#24 — Blank band above the keyboard on the Marketplace search (Frontend, phones)
#24 — Blank band above the keyboard on the Marketplace search (Frontend, phones)
Course player initial load & lesson-complete round-trip (Backend, Update 37)
Course player initial load & lesson-complete round-trip (Backend, Update 37)
GET /training/{id} ships every lesson’s full HTML up front (dominant initial-load cost), and
“Mark as complete & continue” waits on a full GET /user/{id} refetch. Both fixes are specced
in Suggested Changes.Web cold-load: uncompressed canvaskit.wasm + no Cache-Control (Server, Update 33)
Web cold-load: uncompressed canvaskit.wasm + no Cache-Control (Server, Update 33)
#19 — Dead search state in StudentClassroomProvider (Frontend)
#19 — Dead search state in StudentClassroomProvider (Frontend)
filteredLearners / filterLearners / searchController are unread since Update 69 (the
members panel keeps its own search). No effect today; remove in a cleanup pass.#34 — Chapter import skips the chapter's quizzes (Backend)
#34 — Chapter import skips the chapter's quizzes (Backend)
POST /chapters/import creates the chapter and its lessons only: the quizzes of an exported
chapter file (quizzes, or quiz in older files) are not imported, so exporting a chapter and
importing it into another training loses its quizzes. Fix: import each entry of quizzes[]
with the quiz import code of Export_Import_Quizzes_Exams_Controller (row added 19/10/2026, Update 123).#35 — Chapter quizzes cannot be reordered or placed between lessons (Backend + Frontend)
#35 — Chapter quizzes cannot be reordered or placed between lessons (Backend + Frontend)
quizzes.position + POST /quizzes/reorder, then drag
handles on the curriculum’s quiz rows.#36 — Validation errors answer 500 instead of 422 on most endpoints (Backend)
#36 — Validation errors answer 500 instead of 422 on most endpoints (Backend)
validate() inside try { … } catch (\Throwable $th), which returns a 500 with
the message instead of the 422 with per-field errors. The app then shows a server error for a bad
input. Update 124 fixed GET /trainings and GET /quizzes. Fix: rethrow
ValidationException before the \Throwable catch, or validate before the try (row added
19/10/2026, Update 124). 20/10: POST /classroom/create and PUT /classroom/update/{id}
fixed too (Update 131, the slug work).#40 — Chatrooms: Laravel Reverb not running on the servers yet (Server)
#40 — Chatrooms: Laravel Reverb not running on the servers yet (Server)
php artisan reverb:start and
BROADCAST_DRIVER is still log, so the apps poll every 5 seconds instead of receiving messages
instantly. Fix: Reverb keys + BROADCAST_DRIVER=reverb, a supervised reverb:start, and the
nginx /app/ proxy (Chatrooms Setup Guide, section 5).#41 — Chatroom images: server upload limits below 20 MB (Server)
#41 — Chatroom images: server upload limits below 20 MB (Server)
upload_max_filesize /
post_max_size and nginx’s client_max_body_size are much lower, so a large image fails.
Fix: 25M (PHP) and client_max_body_size 25m; (nginx) in Plesk
(Chatrooms Setup Guide, section 3).#55 — Training JSON import builds broken slugs (Backend)
#55 — Training JSON import builds broken slugs (Backend)
POST /trainings/export-import/import
builds the slug from the raw label with a lower-case-only pattern before lowercasing it and cuts
it to 13 characters, so “Imported training” becomes -mported-trai; a taken slug gets a random
suffix instead of -2, -3. Imported trainings get broken URLs and R2 folders. Fix: use
Training::generateUniqueSlug($trainingData['slug'] ?? $trainingData['label']), like POST /training.#63 — Konnect payment init sends the new phone format unchecked (Backend)
#63 — Konnect payment init sends the new phone format unchecked (Backend)
phone_number as
“+216 98378579” (country code, one space, digits). The Konnect init payload sends it as saved;
Konnect’s expected format was not checked and no payment was tried. If Konnect expects 8 local
digits, its checkout may show the number wrong or refuse it. Fix: check against Konnect; if
needed, send only the digits after “+216 ” for a Tunisian number (nothing for a foreign one).#42 — No scheduler cron documented on the servers (Server)
#42 — No scheduler cron documented on the servers (Server)
chatrooms:prune-attachments (unsent chatroom images) and notifications:delete-expired only run
with an every-minute php artisan schedule:run cron, which no deployment note sets up.
Fix: add the cron in Plesk → Scheduled tasks
(Chatrooms Setup Guide, section 3).🔵 Low — cosmetic, noise and technical debt
#4 — test/widget_test.dart is entirely commented out (Frontend)
#4 — test/widget_test.dart is entirely commented out (Frontend)
flutter test always ends with one “failed to load” error. Fix: delete the file or restore
a real test. 09/10: still the only failure — 436 tests pass otherwise (Update 94).'Platform access' reminder puts a password in a stored message (design caveat, Update 94)
'Platform access' reminder puts a password in a stored message (design caveat, Update 94)
notifications table, in the admin Recent log and in the learner’s in-app notifications (and in
any future BCC archive mailbox). Nothing to fix in code — use it knowingly, and replace the
xxxyyyzzz placeholders before sending.#9 — Analyzer debt: 116 issues (0 errors / 21 warnings / 95 infos)
#9 — Analyzer debt: 116 issues (0 errors / 21 warnings / 95 infos)
major_code_updates.md Update 45. The notable buckets:
13 use_build_context_synchronously (real “widget disposed” crash risk — the Konnect
payment flow first), 38 Radio → RadioGroup, 5 ShowCaseWidget (removed in showcaseview
v6 — migrate before upgrading), 7 onReorder → onReorderItem (⚠️ remove the manual index
adjustment when migrating), 12 Matrix4 calls, 12 unused imports, 6 experimental
flutter_quill APIs. 28 of the 116 are dart fix-able.
09/10: after the 08/10/2026 package bump and dead-code batch, flutter analyze reports
0 errors / 10 warnings / 134 infos: the 75 deprecations and 13
use_build_context_synchronously are unchanged, and flutter_lints 6 added 53
strict_top_level_inference infos (members without a declared type, mostly in providers such
as chapter_provider.dart). The ShowCaseWidget / Radio / onReorder buckets above still
apply.#10 — Laravel Pint fails on 4 files (Backend)
#10 — Laravel Pint fails on 4 files (Backend)
CertificationsController.php and the three resources/lang/*/messages.php carry pre-existing
formatting findings. No runtime effect. Fix: a formatting-only commit.#15 — Latin name + period renders mirrored in Arabic (Frontend)
#15 — Latin name + period renders mirrored in Arabic (Frontend)
#25 — 'View certifica…' cut on 360px phones (Frontend)
#25 — 'View certifica…' cut on 360px phones (Frontend)
#26 — Forced-width pages scroll sideways on 11" windows (Frontend)
#26 — Forced-width pages scroll sideways on 11" windows (Frontend)
⚠️ Verification debt (#11)
Updates 53 to 69 (and most later UI work) are verified by widget tests only — the glass cards’ real scrolling cost, the certificate flows, the tab URLs, the remade leaderboard / continue-learning / progress cards, the classroom notify dialog and more have not been looked at in a real browser or on a device, and the realPOST /certification round trip runs through a fake client.
Updates 90 to 94 (the Marketplace Instructors tab, the avatar-menu language row, the instructor
pager arrows, the re-colored admin reminders page) and the 08/10/2026 package bump were likewise
verified through tests and the test renderer only — and the bump still lacks an iOS build and a
manual pass on the screens whose packages changed (drag-and-drop questions, uploads, YouTube
fullscreen on web, 2FA recovery-codes download, the three dropdowns, certificate download on
Android).
✅ Recently resolved (for context)
-
#50 R2 uploads landing flat in
files/whatever folder the app named, and #51 the three backend-only folders outside the layout (chatrooms/{slug}/,files/conversations/,files/instructor_background_items/) → closed on 01/11/2026 (backendR2StoragePathService+ Update 184): the backend builds every key from the ids the app sends (training_id,lesson_id,classroom_id,quiz_id), one folder per training / classroom / quiz / chatroom slug underfiles/; a folder string from a client is never trusted; older objects keep their keys. See R2 Storage Paths. -
Login payload bugs B1 – B4, B6, B7 of the Auth User Optimize plan
→ closed by Update 125 (19/10/2026): progress and attempt ceilings come from the backend, the
Attempts tab loads its rows on open, certificates carry
training_id/quiz_idat boot. -
#33 stale page-title sizes in
student_web_responsive_test.dart→ the expectation was aligned with the code in Update 125. -
GET /auth/userpayload size (1.5 MB, the “App Speed” row 2) → Update 125 (~20 KB, 28 queries). -
Chapter quiz backfill (Update 34 workaround) and one quiz per chapter → closed by Update 123
(19/10/2026): chapters carry
quizzes[]and keep every quiz, and the backfill was deleted. The quiz missing under lessonless chapters was a draft quiz, which the backfill exposed throughGET /chapter/{id}; published quizzes always came back fromGET /training/{id}. - #6 My Content forced to 1536px → fixed by Update 76.
- #18 Classroom by-student payload exposure → fixed by Update 73 (whitelisted persons).
-
#28 Certificates showing the default badge → fixed by Update 82 (
training.badge_logoeager-loaded). - #20 Dashboard “Edit profile information” opening the cart → the widget was deleted in the 08/10/2026 dead-code clean-up (removed, not fixed); the table row can be closed.
-
Marketplace leaks (lesson content / answer keys / instructor PII in student network tabs) →
closed by Updates 39 + 44 (
preview-*endpoints) — the hardening of the old endpoints remains critical (see above). -
Exam views always 0 → fixed by Update 47 (
quizzes.views_countmigration — remember to runphp artisan migrateon each backend).