Skip to main content
The backend has no external monitoring stack yet. What exists today: the public status page, two storage statistics endpoints, a database size query to run by hand, the local storage inspector page, and the Laravel log files. This page also records the one production finding of 09/10/2026 — ≈6.6 GB of orphaned Telescope tables — and the runbook to remove them.

Table of Contents

  • Status page & health check
  • Database size: the query to run
  • Finding 09/10/2026: orphaned Laravel Telescope tables (≈6.6 GB)
  • Removing Telescope safely
  • Local storage inspector (/storage-inspector)
  • Logs

Status page & health check

  • GET / on the backend host is the status page (StatusController): API, database, cache, queue, local storage + storage:link, R2, Turnstile and mail checks, with manual re-checks on /status/mail-check, /status/storage-check and /status/r2-check (throttled per IP).
  • GET /health redirects to GET /api/health.
  • GET /api/local_storage_stats and GET /api/r2_storage_stats give the numbers (partition usage, files area size and growth, R2 free-tier usage) — see File Management & Storage API.

Database size: the query to run

Plesk → Databases → phpMyAdmin → SQL tab. Replace the schema name with the environment’s database (theteamdb_prod, theteamdb_test, theteamdb_dev):
TABLE_ROWS is an InnoDB estimate, not a count. Run it once a month (or after any incident) and compare with the previous result: a normal month moves the real tables by a few MB.

Finding 09/10/2026: orphaned Laravel Telescope tables (≈6.6 GB)

Result of the query above on theteamdb_prod: Two Laravel Telescope tables hold ≈98 % of the database; the whole application data is ≈0.1 GB.
Telescope is not in the backend repository. Checked on 09/10/2026 on LMS_Backend main: no laravel/telescope in composer.json or composer.lock, no config/telescope.php, no TelescopeServiceProvider, no *_create_telescope_entries_table migration, nothing in bootstrap/cache/packages.php, and git log --all -S telescope returns nothing on any branch. The package was either installed directly on the server (outside git) or removed from the code without dropping its tables. Nothing in the application reads those tables, so removing them cannot break the app — once the server is confirmed to match the repository (step 1 below).
Why it grew that big: Telescope records every request, query, job, mail, exception and log line into telescope_entries (one row each, JSON content) and telescope_entries_tags. Without telescope:prune on the scheduler and with TELESCOPE_ENABLED=true, a production API adds tens of thousands of rows a day forever.

Removing Telescope safely

1

Confirm the server matches the repository

On the production server (SSH, or Plesk → Websites & Domains → SSH Terminal), inside the backend root (the folder holding artisan):
All empty / “not found” → Telescope is already gone from the code: skip to step 3. Anything found → Telescope IS installed on the server: do step 2 first.
2

Only if Telescope is installed on the server: remove the package in this order

The order matters. If App\Providers\TelescopeServiceProvider is still listed in config/app.php when the package disappears, every php artisan call (including the package:discover that composer runs after removing a package) dies with “class not found”.
If the server runs from a git checkout, commit these changes to LMS_Backend so the next deploy does not bring Telescope back.
3

Prove nothing writes to the tables any more

phpMyAdmin → SQL. Run it twice, five minutes apart (and after a few API calls). The value must not change:
(sequence is Telescope’s auto-increment key. Avoid COUNT(*) here: on 7.5 M rows it is slow for nothing.)
4

Check that InnoDB stores each table in its own file

ON (the MySQL default since 5.6) → DROP TABLE deletes the table’s .ibd file and the disk space is back immediately. OFF → the rows live in the shared ibdata1; dropping still frees the space for MySQL to reuse, but the file on disk never shrinks (only the hosting provider can rebuild the instance). Either way the drop is safe.
5

Drop the tables (child table first) and forget the migration

No data backup is needed: these rows are request / query debug logs, 6 GB of them, useless once Telescope is gone. If you want the empty structure back one day, a schema-only dump is enough and takes a second: mysqldump --no-data theteamdb_prod telescope_entries telescope_entries_tags telescope_monitoring > telescope_schema.sql (a full Plesk “Export Dump” would be 6+ GB — do not bother).
DROP TABLE on a 6 GB table takes a few seconds (it unlinks a file); run it off-peak anyway. If phpMyAdmin shows a timeout, the statement still finishes on the server: refresh and check SHOW TABLES LIKE 'telescope%'; instead of running it again.
6

Verify

Re-run the size query. Expected: the biggest table is now quiz_questions (≈33 MB) and the whole schema is ≈0.1 GB. Then open the status page and a few API endpoints — nothing in the app touches the dropped tables, so nothing changes.
Do not use php artisan telescope:prune or telescope:clear to empty the tables: they delete 1,000 rows per iteration and would run for hours on 7.5 M rows — and they only exist while the package is installed. DROP TABLE (or TRUNCATE, below) does the job in seconds.

Alternative: keep Telescope, but bounded

Only if Telescope is still installed on the server and you want to keep it for debugging:
Then empty the existing tables once (TRUNCATE recreates the table, so the space comes back like a drop; the foreign key check must be off for the parent table):

Local storage inspector (/storage-inspector)

Added 09/10/2026 (LMS_Backend: StorageInspectorController, app/Services/StorageInspectorService.php, resources/views/storage_inspector.blade.php, app/Console/Commands/StorageInspectorUrl.php, tests/Feature/StorageInspectorTest.php). A read-only, paginated HTML page answering “what is in the local storage and who uses it?”. It never deletes or moves anything.

What it lists

Every file under storage/app (the local disk): the uploads in public/files/…, the bulk-upload folders public/files/<folder>/…, and the certificate share images in certificate-images/…. For each file: References are resolved from:
  • Foreign keys to files.id: trainings.image_id / video_id / badge_logo_id, quizzes.image_id, quiz_questions.image_id, users.image_id, companies.company_image_id, certification_configs.company_logo_id / signature_id / cachet_id, landing_pages.header_image_id / header_background_image_id / banner_image_id, landing_page_partner_companies.company_logo_id, landing_page_featured_categories.feat_category_image_id, user_background_items.logo_id, platform_theme_settings.platform_logo_id.
  • URL columns: platform_theme_settings.platform_logo / auth_page_image, certification_configs.default_badge_logo, landing_page_partner_companies.company_logo.
  • Rich text that embeds /storage/... links: lessons.content (reported with the lesson’s training), classrooms.content / classroom_description, trainings.description, quizzes.description, quiz_questions.label, landing_pages.company_description, users.public_profile_description.
  • Naming conventions: certificate-images/<certification_id>-<fingerprint>.png (CertificateImageService cache, regenerated on demand) and public/files/2fa-<user_id>-<time>.png (the 2FA enrolment QR codes).
Statuses: Trust the page in one direction only: a file it marks used is used. A file it marks orphan could still be referenced by something the index does not know (a hard-coded URL in the frontend, a lesson whose link does not contain /storage/), so look before deleting.

Access

The page answers 404 unless one of these is true: The route is throttled (30 requests / minute / IP) and carries noindex. Paging and filters keep the token or signature on every link.

Filters & options

q (path, URL, model name, slug, label or #id), status, area (folder), sort (modified · size · name · status · area), dir, per_page (10–200, default 50), page. The disk scan and the database index are cached for STORAGE_INSPECTOR_CACHE_SECONDS (default 60); refresh=1 — the rescan now link — rebuilds them.

Checks


Logs

  • Laravel writes to storage/logs/laravel.log (LOG_CHANNEL=stack → single file by default; the daily channel keeps 14 days). On Plesk: Files → httpdocs/storage/logs/, or Logs for the web server’s access / error logs.
  • LOG_LEVEL=debug is the .env.example default; use error or warning in production so the file stays readable.
  • The application also keeps its own tables: history (audit log, see History & Activity Log API), email_logs, authentication_log, konnect_history. They grow slowly; the size query above tells when one of them needs pruning.