# Bug fix — Issue #1597: one `codetrackers` row with an unknown type 500s the whole Code Trackers grid (empty body)
## Symptom
The modern **Code Trackers** screen (`gui/templates/codetracker/codetrackerView.html`) went blank
whenever **a single** row in `codetrackers` had a `type` outside
`{1 = stash/rest, 200 = github/rest}` (`lib/functions/tlCodeTracker.class.php:28-29`):
- `GET /api/codetracker/index.php` → **HTTP 500 with a 0-byte body** (not even a JSON error),
- the grid rendered its 6 column headers with **no rows and no footer**,
- 6 `log_level = 2` rows landed in the Event Viewer per page load,
- the fatal happened inside `getAll()`, i.e. **after** the view/manage gate, so it hit **every**
role — a `codetracker_view`-only user included.
The state is reachable through an import/migration, a hand-edited DB, or an implementation removed
in a later release. The modern BFF itself cannot create it: `POST`/`PUT` validate the type against
`getSystems(['status' => 'enabled'])` (`api/codetracker/index.php:308`,
`rejectInvalidTrackerType()`), and the stored-token routes re-check at `:463` and in
`githubInterfaceFor()` `:370`.
## Environment and fixtures
- TestLink 2.0.1, PHP 8.3 built-in server on `http://localhost:8082` (docroot = repository root),
stderr → `tmp/php_server.log`; MariaDB `127.0.0.1:3306`, db/user/password `testlink`.
- Freshly imported DB — `codetrackers` was **empty** (0 rows) at session start, so the fixtures
were recreated:
```sql
INSERT INTO codetrackers (name,type,cfg) VALUES ('Bad Type Tracker',5,'');
INSERT INTO codetrackers (name,type,cfg) VALUES ('Stash Valid',1,'http://127.0.0.1:7999/');
INSERT INTO codetrackers (name,type,cfg) VALUES ('GitHub Valid',200,'https://api.github.como/rmainsecret123');
```
- Login `admin` / `admin`; browser checks in headless Chrome, API checks with `curl` +
`X-Requested-With: XMLHttpRequest` and a session cookie.
## Root cause
One missing `isset()` guard, amplified by an unguarded static call:
```
codetrackers.type = 5 (not a key of tlCodeTracker::$systems = {1,200})
└─ api/codetracker/index.php:254 getAll(['output'=>'add_link_count','checkEnv'=>true])
└─ tlCodeTracker.class.php:568 $impl = getImplementationForType($item['type'])
└─ tlCodeTracker.class.php:115 $spec = $this->systems[$codeTrackerType]; ← NO isset GUARD
:116 E_WARNING Undefined array key 5
:120 E_WARNING Trying to access array offset on null
:124 E_WARNING Trying to access array offset on null (×2)
returns the literal string "Interface"
└─ tlCodeTracker.class.php:569 $impl::checkEnv()
└─ autoloader lib/functions/common.php:122 include_once('Interface.class.php')
→ 2 × E_WARNING "Failed opening …class.php"
→ Uncaught Error: Class "Interface" not found → script aborts before out()
```
Measured pre-fix, `tmp/php_server.log`:
```
PHP Fatal error: Uncaught Error: Class "Interface" not found in …/lib/functions/tlCodeTracker.class.php:569
Stack trace:
#0 …/api/codetracker/index.php(254): tlCodeTracker->getAll()
[500]: GET /api/codetracker/index.php
```
`checkEnv => true` is not incidental: it is the option that makes the **Environment** column work —
requested by the legacy list (`lib/codetrackers/codeTrackerView.php:23`) and re-added to the BFF by
issue #973 ("restore the Environment column"). So the very option that fixed the column walked into
the fatal. `php -l` cannot see this: it is a runtime class-resolution error.
### Blast radius of the un-guarded call
| Call site | Reachable with an out-of-map type? | Before the fix |
|---|---|---|
| `tlCodeTracker::getAll():568` (list, `checkEnv`) | yes, from DB rows | **fatal 500, empty body** (this issue) |
| `tlCodeTracker::getByID():358` (detail / edit modal) | yes, from DB rows | 4 warnings + `implementation: "Interface"` in the JSON |
| `lib/codetrackers/codeTrackerView.php:23` (legacy view) | yes, from DB rows | same fatal on the legacy page (upstream origin) |
| `lib/ajax/getcodetrackercfgtemplate.php:24` | no — pre-guarded (`isset($ctt[$type])`) | `codetracker_invalid_type` |
| `lib/codetrackers/codeTrackerCommands.class.php:265` | only from the legacy edit controller, which validates first | pre-existing, unreachable with a bogus type |
| `api/codetracker/index.php:308,370,375,463` | no — all pre-validate | 400 with a clear message |
## The fix
### 1. `lib/functions/tlCodeTracker.class.php` — guard in the library, degrade per row
`getImplementationForType()` now returns `null` for a type that is not a key of `$systems`, **before**
`$spec` is dereferenced (`:115-127`). One guard covers both unguarded call sites, including the legacy
view; `getByID():358` then yields `implementation: null`, which `trackerToJSON()`'s existing
`$item['implementation'] ?? ''` maps to `""`.
`getAll()`'s `checkEnv` block (`:578-599`) no longer calls the implementation blindly:
```php
if( is_null($impl) || !@class_exists($impl) || !method_exists($impl, 'checkEnv') )
{
$item['env_check_ok'] = false;
$item['env_check_msg'] = '';
}
else
{
$dummy = $impl::checkEnv();
...
}
```
so one bad row degrades **only itself** and every other row keeps being listed. `class_exists()` is
`@`-silenced on purpose: the autoloader `include_once()`s `.class.php`
(`lib/functions/common.php:122`) and would otherwise re-log the two `Failed opening …class.php`
warnings for a *known* type whose file is missing — the same reasoning already applied to the
requirement-manager list route (`api/reqmgrsystems/index.php:107`, issue #1593).
### 2. `api/codetracker/index.php` — `typeKnown` (one field)
`trackerToJSON()` adds `'typeKnown' => $typeLabel !== ''`. `$typeLabel` is built from
`$mgr->systems[$item['type']]` and is non-empty for every known type (`"stash (Interface: rest)"`),
so it doubles as the "is this type in the map" flag: no extra lookup, no string sniffing in JS.
### 3. `gui/templates/codetracker/codetrackerView.html` — make the cause visible
The issue complained that "the UI shows nothing about it". `renderTable()` now renders
`TLi18n.t('ct.msg.invalidType', {type: t.type})` in a red `badge-env-ko` badge in the **Type** cell
when `typeKnown === false`, and the **Environment** cell keeps its `ct.envKo` fallback (the degraded
row arrives with an empty `env_check_msg`).
**No new i18n key was introduced** — `ct.msg.invalidType` already exists in all 10 locale bundles
(`en, de, es, fr, it, ja, pt, ro, ru, zh`) with its `{type}` placeholder. The interpolated string goes
through `esc()` like every other cell, preserving the #1581 escaping contract. The stale
`tlCodeTracker.class.php:566-572` line reference in the neighbouring comment was updated to
`:578-599`.
## Alternatives rejected
- **Guard only in the BFF** (probe `checkEnv` in the route the way
`api/reqmgrsystems/index.php:97-113` does): leaves 4 offset warnings per row per load (Event-Viewer
noise) and leaves the legacy view and `getByID()` broken. The guard belongs in the library, once.
- **Throw / return a dummy class** for an unknown type: still a 500, just with a nicer message — the
grid would still be lost for every viewer.
- **Delete or auto-heal the row from the read path**: a read route must never write (the invariant the
#970 write gate established), and a row can legitimately be unknown after an implementation is
dropped in a future version.
## Verification (fresh DB, fixtures above)
| # | Case | Expected | Measured | Verdict |
|---|---|---|---|---|
| 1 | row `type=5` | 200, row listed, `env_check_ok=false`, `typeKnown=false`, no new events | `API HTTP 200 len=314`; `rows:3`, footer `Showing 1 to 3 of 3 entries`, Type cell `Code Tracker type 5 is unknown.`, Environment cell `Environment check failed`; `events WHERE log_level=2` = 6 (unchanged) | PASS |
| 2 | row `type=1` | green OK | `stash (Interface: rest)` + `OK` | PASS |
| 3 | row `type=200` | green OK | `github (Interface: rest)` + `OK` | PASS |
| 4 | `GET /api/codetracker/index.php/1` | 200, `implementation: ""` | `HTTP 200`, `implementation === ""` | PASS |
| 5 | `POST /api/codetracker/index.php/1/test_connection` | 400 | `{"status":"error","message":"Unknown code tracker type"}` | PASS |
| 6 | screen in the browser | row visible with 2 diagnostics, no console error | console: no messages | PASS |
| 7 | manager repair path | the row is fixable from the UI | edit modal → type `stash` → Save → DB `type` 5 → **1**; row then green | PASS |
| 8 | static gates + log | clean | `php -l` ×2, all 6 `