> ## Documentation Index
> Fetch the complete documentation index at: https://theteam-13fc7f49.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Monitoring & Logs (laravel telescope & custom storage inspector)

> Watching the production database size, removing the Laravel Telescope leftovers found on 09/10/2026, the read-only local storage inspector page, and where the backend logs are.

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](https://theteam-13fc7f49.mintlify.site/api-reference/file-management-and-storage-api.md).

***

## **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`):

```sql theme={null}
SELECT
    TABLE_NAME                                                   AS `Table`,
    ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024, 3)  AS `Size (GB)`,
    ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2)         AS `Size (MB)`,
    ROUND(DATA_LENGTH  / 1024 / 1024 / 1024, 3)                  AS `Data (GB)`,
    ROUND(INDEX_LENGTH / 1024 / 1024 / 1024, 3)                  AS `Index (GB)`,
    TABLE_ROWS                                                   AS `Approx. Rows`
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'theteamdb_prod'
  AND TABLE_TYPE = 'BASE TABLE'
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC
LIMIT 5;
```

`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`:

| Table | Size | Data / Index | Approx. rows |
| :- | -: | -: | -: |
| `telescope_entries` | **6.019 GB** | 5.075 / 0.943 GB | 7,470,127 |
| `telescope_entries_tags` | **0.571 GB** | 0.321 / 0.250 GB | 4,661,327 |
| `quiz_questions` | 0.032 GB | 0.027 / 0.005 GB | 57,966 |
| `quiz_question_options` | 0.030 GB | 0.025 / 0.005 GB | 239,599 |
| `quiz_student_answers` | 0.005 GB | 0.001 / 0.004 GB | 17,252 |

Two [Laravel Telescope](https://laravel.com/docs/10.x/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`):

```bash theme={null}
composer show laravel/telescope          # expected: "Package laravel/telescope not found"
grep -rn -i telescope composer.json config/app.php app/Providers .env bootstrap/cache/*.php
php artisan route:list | grep -i telescope   # expected: nothing
ls config/telescope.php app/Providers/TelescopeServiceProvider.php public/vendor/telescope 2>/dev/null
```

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".

```bash theme={null}
# a) stop recording right now
#    .env → TELESCOPE_ENABLED=false
php artisan config:clear

# b) remove the registration BEFORE removing the package
#    - config/app.php  → delete the App\Providers\TelescopeServiceProvider::class line (Laravel 10 lists providers there)
#    - app/Providers/AppServiceProvider.php → delete any $this->app->register(TelescopeServiceProvider::class) block
#    - composer.json → delete "laravel/telescope" from "extra.laravel.dont-discover" if present
rm -f app/Providers/TelescopeServiceProvider.php config/telescope.php
rm -rf public/vendor/telescope
rm -f database/migrations/*_create_telescope_entries_table.php

# c) remove the package and rebuild the caches
composer remove laravel/telescope --no-interaction
php artisan optimize:clear
php artisan route:list | grep -i telescope   # must print nothing
```

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:

```sql theme={null}
SELECT MAX(sequence) AS last_entry FROM telescope_entries;
```

(`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**

```sql theme={null}
SHOW VARIABLES LIKE 'innodb_file_per_table';
```

`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).

```sql theme={null}
-- telescope_entries_tags has a foreign key on telescope_entries: drop the child first
DROP TABLE IF EXISTS telescope_entries_tags;
DROP TABLE IF EXISTS telescope_entries;
DROP TABLE IF EXISTS telescope_monitoring;

-- forget the migration record so migrate:status / migrate:rollback never look for it
DELETE FROM migrations WHERE migration LIKE '%create_telescope_entries_table%';
```

`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:

```bash theme={null}
# .env (production)
TELESCOPE_ENABLED=false        # record nothing in production (or keep true and prune daily)
```

```php theme={null}
// app/Console/Kernel.php — keep 48 hours of entries at most
$schedule->command('telescope:prune --hours=48')->daily();
```

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):

```sql theme={null}
SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE telescope_entries_tags;
TRUNCATE TABLE telescope_entries;
TRUNCATE TABLE telescope_monitoring;
SET FOREIGN_KEY_CHECKS = 1;
```

***

## **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:

| Column | Content |
| :- | :- |
| **File** | path, size, last modified, an **open** link (`APP_URL/storage/...` for the public files), a thumbnail for images |
| **Registry** | the matching `files` row(s): `File #id`, type, company, uploading user, URL, and whether the row is active / archived / soft-deleted. Matched by `file_name` (`public/files/<file_name>`, or `public/<file_name>` for bulk uploads) and by the `/storage/...` part of `url` |
| **Used by** | every row pointing at that file, with the model name, **id**, **slug** (trainings, exams), label and lifecycle state — plus the parent for nested content (the training of a lesson, the quiz of a question) and the column it came through |
| **Status** | `used`, `inactive`, `unused`, `orphan` or `missing` (legend below) |

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:

| Status | Meaning | Typical action |
| :- | :- | :- |
| `used` | referenced by at least one active row | keep |
| `inactive` | referenced only by archived / soft-deleted rows | keep until those rows are purged |
| `unused` | has a `files` row, nothing points at it | the row and the file can go (`DELETE /api/delete_file/{id}` removes both) |
| `orphan` | on disk, no `files` row, no reference | leftovers of failed uploads, old 2FA QR codes, hand-copied files: delete by hand after a look |
| `missing` | a `files` row (type internal) whose file is gone from the disk | the model pointing at it shows a broken image: re-upload or clear the reference |

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:

| Way | How |
| :- | :- |
| Token | `.env` → `STORAGE_INSPECTOR_TOKEN=<long random value>` (`openssl rand -hex 32`), then `php artisan config:clear` (or `config:cache` if the config is cached). Open `https://<backend-host>/storage-inspector?token=<value>`; the header `X-Storage-Inspector-Token` works too |
| Signed link | `php artisan storage:inspector-url --minutes=60` prints a temporary link. It is bound to `APP_URL`: open it on exactly that host |
| Local | `APP_ENV=local` opens it without anything |

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

```bash theme={null}
DB_CONNECTION=sqlite DB_DATABASE=:memory: php artisan test --filter=StorageInspector
```

***

## **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](https://theteam-13fc7f49.mintlify.site/api-reference/history-activity-log-api.md)), `email_logs`,
  `authentication_log`, `konnect_history`. They grow slowly; the size query above tells when one of
  them needs pruning.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.