# Admin UI review ledger — tnt_mcp

Every UI/UX suggestion ever made about this plugin's admin surface, with a verdict. **Read this
before proposing anything.** Its whole purpose is that a suggestion is argued once: a rejected idea
stays rejected with its reason attached, and a shipped idea is not re-proposed as if it were new.

## How a round works

1. `NODE_PATH=<shop>/plugins/tnt_international_phone/tests/e2e/node_modules OUT=<dir> node
   tests/e2e/shot_admin_tabs.js` — every tab, light **and** dark. A `MISS` line means that file
   shows the first tab, not the named one; never review a MISS.
2. A reviewer (Opus 5 or Fable) gets the screenshots **and this file**, and proposes only what is
   not already here.
3. New rows land below with verdict `proposed`. Tim's call, or an obvious call, moves them to
   `accepted` / `rejected`; `shipped` once the code is in and re-screenshotted.

## Verdicts

| Verdict | Meaning |
|---|---|
| `proposed` | On the table, undecided. |
| `accepted` | Agreed, not built yet. |
| `shipped` | Built and verified on a screenshot. |
| `rejected` | Decided against — the reason is binding on future rounds. |
| `deferred` | Right idea, wrong time; the row says what unblocks it. |

## Standing constraints (a proposal that violates one of these is out before it is argued)

- Tailwind utility classes + TailAdmin patterns only. **No hand-written CSS** (Tim, standing rule).
- Only classes already emitted into the built CSS exist at runtime. A new class needs a CSS
  rebuild, so a proposal that leans on exotic utilities must say so.
- `bg-white` is unusable: the shop admin ships `.bg-white{background-color:#fff!important}`, which
  beats `dark:bg-*`. Use `bg-white/5`, `bg-gray-50`, etc. (burned on tnt_workflows 1.24.1).
- Every surface must read correctly in light **and** dark. Both are screenshotted every round.
- Smarty + HTMX. No SPA framework, no client-side router.

## Ledger

| ID | Date | Surface | Suggestion | Verdict | Reason / commit |
|----|------|---------|-----------|---------|-----------------|
| U1 | 2026-08-05 | Tabs (whole plugin) | Merge Server-Status + Integrationen into one **Start** tab that answers "what do I do next?" | shipped | Design step 11, 0.27.0. `adminmenu/Controller/Start.php` + `start.tpl`; `ServerStatus`/`Integrations` and their templates deleted, both old tab names still routed. The endpoint URL is now rendered in exactly one place in the plugin. |
| U2 | 2026-08-05 | Integrationen | Replace the full-width orange rights-warning wall with one compact row + "Mehr erfahren" | shipped | Design step 11, 0.27.0. One row on Start; the standing warning and the live-connections table sit behind "Mehr erfahren". The rights-audit alert for the current admin stays a full alert — it names something that is wrong now. |
| U3 | 2026-08-05 | Anfrage-Protokoll, Tokens & Audit | Default to 50 rows, put arg/scope token walls behind per-row toggles, do not render empty sections | accepted | Fable design §6.8. Both pages are 6,900–8,300 px of raw dump shipped as merchant UI. |
| U4 | 2026-08-05 | Tabs | Merge Anfrage-Protokoll + Änderungen into one **Aktivität** tab with an HTMX sub-nav | shipped | Design step 12, 0.26.0 (`f110945`). Plain links rather than an HTMX sub-nav — see `Controller/Activity.php` for why. The row was left `accepted` when it shipped. |
| U5 | 2026-08-05 | New KI tab | One card, progressive disclosure: provider/key/model always open, cost brake collapsed, API address only rendered for "OpenAI-kompatibel", single primary "Speichern & testen" | accepted | Fable design §3. Testing must stop being an optional second click — most merchants save an untested key. |
| U6 | 2026-08-05 | New KI tab | Status banner with four states (Nicht eingerichtet / Aktiv / Eingeschränkt / Gestört), fed passively by live calls + 15-min lazy TTL | accepted | Fable design §4 = Tim's explicit ask for a status section. |
| U7 | 2026-08-05 | KI form | Delete the "Preis-Überschreibung (USD je 1 Mio. Token)" field with its packed `3.00/15.00` syntax | accepted | Fable design §6.5. Developer syntax in a merchant form; the runs-per-hour brake already covers unpriced models. |
| U8 | 2026-08-05 | Tokens & Audit | Rename the tab to **Zugriff** and hide empty sections (e.g. "Statische Token 0/0") | accepted | Fable design §2. "Tokens & Audit" names the implementation, not the question the merchant has. |
| U9 | 2026-08-05 | Server-Status, Tokens & Audit, Integrationen — **light** | `span.badge.badge-info` scope chips are white on pale blue (~2.5:1). Replace with `inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium bg-sky-500/10 text-sky-700 dark:bg-sky-400/10 dark:text-sky-300` | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. Scope walls are the dominant content of Token & Clients; unreadable in light. Needs `/10` tint utilities — check the built CSS. S |
| U10 | 2026-08-05 | All tabs — both | Bare `<code>` inherits Bootstrap's `color:#e83e8c`, so endpoint URLs, all request-log endpoint cells and token IDs render magenta. Swap for `font-mono text-xs text-gray-700 dark:text-gray-300`; keep red for real errors only | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. A full column of red monospace reads as "everything failed". S |
| U11 | 2026-08-05 | Änderungen — **dark** | `btn-outline-secondary` (`changes.tpl:196` Details, `:25` Zurück zur Liste) is dark-gray on dark. Use `border-gray-400 text-gray-600 hover:bg-gray-500/10 dark:border-gray-500 dark:text-gray-300` | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. It is the only route into a change's undo view. S |
| U12 | 2026-08-05 | Integrationen — both | Six "tool … dropped" notices stack in one Status cell (`integrations.tpl:176-178`), dwarfing "aktiv". Collapse behind `<details>` "6 Hinweise", amber-700/300 | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. Developer diagnostics shown to a merchant; `text-warning` on white is also ~2:1. M |
| U13 | 2026-08-05 | Integrationen — both | Provider accordion (`integrations.tpl:100`) has no expand affordance — the connect URL is its most important content and looks like a static status row. Add a chevron or an explicit "Anleitung anzeigen" link | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. S |
| U14 | 2026-08-05 | Integrationen — both | Google Gemini and Microsoft Copilot get full cards with red dots despite nothing being actionable. Demote to one muted line "Nicht unterstützt: …" | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. Red over-alarms; nothing is broken. S |
| U15 | 2026-08-05 | Anfrage-Protokoll — both | `legacy` auth is painted the same green as `oauth` (`request-log.tpl:35-36`), hiding exactly the connections the plugin wants migrated. Give legacy an amber chip + title | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. S |
| U16 | 2026-08-05 | Integrationen — both | `integrations.tpl:5-17` is a hand-written `<style>` block with hex colours (`#6b7280` not theme-aware) — violates this file's own standing constraint. Express dots/toggle/status as utilities | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. M |
| U17 | 2026-08-05 | Integrationen — both | Werkzeuge cell (`:181-184`) is ~25 tool names as pink monospace with only `mr-1`. Wrap in `flex flex-wrap gap-1`, neutral mono chips | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. S |
| U18 | 2026-08-05 | Tokens & Audit — both | UUID-length client/token IDs bloat the table and cannot be copied. `max-w-[8rem] truncate` + full value in `title` + a copy button | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. M |
| U19 | 2026-08-05 | Integrationen — both | Plugin/Werkzeuge cells centre while the tall Status cell starts at the top, so the row looks broken. Add `align-top` to all three `<td>`s | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. S |
| U20 | 2026-08-05 | Änderungen — both | With few rows the full-width card leaves ~500 px of dead space and scatters the badge trio. Constrain to `max-w-5xl`, group ERGEBNIS+STATUS into one cell | shipped | Ruling 21 (2026-08-05), 0.23.0. Fable round 1. M |

| U21 | 2026-08-05 | KI tab | Build the tab in Tailwind on the plugin's own build, `.tailwind`-scoped, both themes | shipped | Ruling 20. `adminmenu/{package.json,tailwind.config.js,css/}` + `make css`; `BaseAdminController::fetch()` wraps every tab. |
| U22 | 2026-08-05 | KI tab | Inputs need `form-input` / `form-select` explicitly | shipped | The forms plugin is built with `strategy: 'class'`, so an unclassed input inherits the admin's styling and renders as an INVISIBLE box — caught on the round-2 screenshot, not by any test. Binding on every future field. |
| U23 | 2026-08-05 | KI tab | Do not render the tester's raw provider message under the form | shipped | It is English in a German admin and repeats the mapped sentence in the banner. Two error lines, one in the wrong language, is worse than one. |
| U24 | 2026-08-05 | Tokens & Audit — both | Expired OAuth tokens are painted green `aktiv` while Server-Status counts them as 0/11 active — the two tabs contradict each other. Branch on expiry in `tokens-audit.tpl`: revoked → `neutral` "widerrufen", expired-not-revoked → `neutral` "abgelaufen", `success` only when genuinely live (controller passes an `expired` bool) | shipped | Fable round 2 (2026-08-05), fixed in 0.24.1. ⚠️ This is a correctness bug, not a style one: the tab answers "who has access" with eleven dead tokens shown as live. S |
| U25 | 2026-08-05 | Anfrage-Protokoll — both | The Endpunkt column says `tools/call` two hundred times; the tool name is buried mid-cell in `tool=… args=…`. Promote the tool name to the cell's primary `<code>` with the endpoint demoted beneath, leaving only args in Parameter | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). "What happened?" is the tool name, and it is the one thing that cannot be scanned. Survives the accepted U3/U4 restructure. M |
| U26 | 2026-08-05 | Tokens & Audit — both | Five OAuth clients all called "ChatGPT", distinguishable only by a truncated UUID. Add a "Zuletzt benutzt" column and mute clients with no live token (`neutral` chip "inaktiv") | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). The only per-client action is Löschen, and today picking the right one is a guess. M |
| U27 | 2026-08-05 | Anfrage-Protokoll — both | `auth_challenge` rows get a red `401` chip although that 401 IS the OAuth handshake — the log opens with two apparent failures. Neutral chip + title for that endpoint; danger stays for real 4xx/5xx | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). Painting by-design rows red trains the merchant to ignore the colour that matters. S |
| U28 | 2026-08-05 | Server-Status — both | Audit-Zusammenfassung lists raw outcomes (`refused:blocked_action`) in nondeterministic order — light and dark shots of the same data differ. Sort by count desc in the controller, map prefixes to tone chips + German labels | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). Unstable ordering makes two visits look like different data. M |
| U29 | 2026-08-05 | Tokens & Audit — both | In the Audit-Log, ERGEBNIS sits after ARGUMENTE, so wide JSON pushes the outcome off-viewport entirely. Move the Ergebnis column before Argumente | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). Column order only; stays correct after the accepted U3 arg toggles. S |
| U30 | 2026-08-05 | KI — both | The banner says "Anbieter fehlt" while the Anbieter select below already shows "OpenAI". Render a disabled selected placeholder option when the stored provider is null | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). First run is this tab's most common state, and the page contradicts itself in it. S |
| U31 | 2026-08-05 | Server-Status — both | Label cells wrap to three lines while the value column sits nearly empty (auto 50/50 split). Give each label `<th>` a fixed `w-56` | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). S |
| U32 | 2026-08-05 | Änderungen — both | The action title is inert; the only route to the detail is the small Details button. Link the title too, keeping the button | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). S |
| U33 | 2026-08-05 | KI — both | The KOSTENBREMSE `<details>` uses the browser's default ▶ marker while Integrationen uses the styled rotating chevron — two disclosure languages in one plugin | shipped | Ruling 22, 0.25.0. Fable round 2 (2026-08-05). The default marker also renders differently per browser. S |

## Settled questions (do not re-open)

- **Bootstrap vs Tailwind → Tailwind** (Tim, ruling 20, 2026-08-05). tnt_mcp shipped no CSS at all;
  its tabs inherited the shop admin's Bootstrap while tnt_workflows renders its own compiled
  Tailwind, which is why the two plugins read as different products. The U9–U20 pass converted the
  five remaining tabs in 0.23.0 — **no Bootstrap class is left in adminmenu/templates/**, and a new
  one must not be introduced.
- **The class vocabulary lives in `adminmenu/Controller/Ui.php`**, assigned to every template as
  `$ui` by `BaseAdminController::fetch()`. A tab does not invent its own card/table/chip classes; a
  new shared surface is added there (the file is scanned by tailwind.config.js, so a class named
  only in PHP still gets emitted). A view model names a *tone* (`success`, `warning`, `danger`,
  `neutral`, `info`) and the template maps it onto `$ui.chip` — PHP never returns a CSS class.
- **`/10` opacity tints exist.** The plugin's own build scans its own templates, so any utility a
  template uses is emitted; `make css` after every template change is the only requirement.

## Harness caveat (found 2026-08-05, FIXED)

Round 1 reported that both `einstellungen` screenshots showed Server-Status content although
`shot_admin_tabs.js` printed `ok`. The script only checked that an anchor with that label had been
clicked. It now reads the active `.tab-pane` AFTER the animation and compares it to the anchor's
target, and correctly reports `MISS — not a pane anchor (href="#")` for Einstellungen, which is a
JTL `<Settingslink>` and not a pane at all. **Einstellungen cannot be screenshotted by this script
and remains unreviewed**; it needs its own navigation (the settings page is a separate URL).
| U34 | 2026-08-06 | Zugriff (Audit-Log) — both | Empty argument lists print the encoder's literal `[]` / `{}`, so a successful no-arg call (`list_snippets`, `list_schedules`) reads like a truncated payload. Blank them in `TokenAuditOverview::auditEntries()` and render a muted en-dash | shipped | Ruling 26, 0.27.1. Fable round 3 (2026-08-06), dark reviewer. Distinct from accepted U3, which is about hiding *walls* of args — this is the empty case. S |
| U35 | 2026-08-06 | Zugriff (Audit-Log) — both | ERGEBNIS exact-matches `ok` and sends everything else to the danger chip, so the merchant's SUCCESSFUL writes (`ok:coupon.coupon.create`, `ok:shipping.surcharge.save`) render in the same red as `refused:*` and red stops meaning "broken". Tone via the existing `AuditOutcome::tone()` prefix map, as U28 already does on Aktivität | shipped | Ruling 26, 0.27.1. Fable round 3 (2026-08-06) — BOTH reviewers raised it independently, highest-ranked finding of the round. The action id stays in the chip text so rows remain greppable. M |
| U36 | 2026-08-06 | Aktivitaet (Anfragen) — both | A pre-auth `tools/list` probe answered 401 opens most client sessions and renders as a red STATUS chip, though it is the normal MCP connect/re-auth handshake. U27 fixed this class but its `handshake` flag in `RequestLogView.php` covers only the `auth_challenge` endpoint | shipped | Ruling 26, 0.27.1. Fable round 3 (2026-08-06) — both reviewers. ⚠️ Fix must stay NARROW: auth `invalid` can also be a genuinely bad token, which must keep its danger chip. Neutralise only when no credentials were carried, or when the 401 immediately precedes a successful connect from the same client. Fixed NARROWLY in `RequestLogView::isHandshake()`: 401 + auth `none` + endpoint `tools/list`/`initialize`. Auth `invalid` deliberately still danger — a presented-and-rejected token is a real problem. M |
| U37 | 2026-08-06 | Start — both | Disclosure chevrons sit right-aligned on the provider cards and rights row (`start.tpl` `ml-auto`) but left of the label on "Technische Details" — two placements on one page, against U33's one-disclosure-language rule | shipped | Ruling 26, 0.27.1. Fable round 3 (2026-08-06), light reviewer. Chevron moved to `ml-auto` on "Technische Details". S |
| U38 | 2026-08-06 | Start — dark | The NÄCHSTER SCHRITT banner repeats checklist item 2 verbatim (title *and* sentence) ~60 px above it, reading as a rendering bug | rejected | Fable round 3 (2026-08-06), dark reviewer. VERIFIED against `start.tpl:8-9`: the repetition is deliberate and documented — the banner repeats only the ONE step due now, because "a list of five items with three ticks is still a list". The light reviewer read it the same way. No change. S |
| U39 | 2026-08-06 | KI — both | Saving a new connection is impossible: the store refused a blank model, but the model list can only be fetched with a key already stored, so the merchant is asked for something they cannot know yet. Allow a blank model on save (incomplete, not rejected), fetch the list right after, and offer it as clickable chips under the field | shipped | Tim, testing 0.27.1 (2026-08-06): "A default model must be given. But at this point, I still dont have the model list to select from". Fixed in 0.27.2. The MCP tool `set_ai_configuration` keeps model mandatory — an assistant has `list_available_models`. M |
| U40 | 2026-08-06 | Aktivitaet (Anfragen), Zugriff (Audit-Log) — both | The 200-row log tables run several screens tall, so everything below them is unreachable without scrolling past the whole log. Cap at 400px with `overflow-y-auto` and a sticky header row | shipped | Tim, testing 0.27.1 (2026-08-06). `Ui::TABLE_SCROLL` + `Ui::TH_STICKY`, applied to those two tables only. S |
| U41 | 2026-08-06 | Dokumentation — both | The tab renders README.md, which was the developer README: migration class names, PHP classes, curl handshakes. A shop admin is not the audience | shipped | Tim, testing 0.27.1 (2026-08-06): "remove text that is too technical (exact Migration names etc). The shop admin might be a not so technical user". README.md rewritten merchant-facing (setup, what the assistant may/may not do, safety, troubleshooting); the developer text moved to `docs/DEVELOPMENT.md`. L |
| U42 | 2026-08-06 | Einstellungen (JTL `<Settingslink>` pane) — both | The plugin's compiled Tailwind carries preflight, which is global; it zeroed the margins and line-height of the shop's OWN settings pane (all panes share one document), leaving the heading glued to the tab strip with no padding | shipped | Tim, testing (2026-08-06), reported on tnt_workflows and identical here. Repaired scoped to `.settings .settings-content` in `adminmenu/css/tailwind.css`. Not fixable by `important: '.tailwind'` — preflight is base-layer element selectors. M |
| U43 | 2026-08-06 | All tabs — both | U42's repair left the settings pane at 1rem/1.25rem — measurably present but tighter than the rest of the admin, so it still read as "no padding" | shipped | Tim, testing 2.0.0 (2026-08-06): "i still have no padding or margins". Measured live BEFORE the change: 16/20/20px. Raised to 1.5rem/1.5rem/1.75rem + 0.5rem under `.subheading1`; measured live AFTER: 24/28/24px. S |
| U44 | 2026-08-06 | Aktivitaet, Zugriff, Zugriff→Änderungen, Start — both | U40 capped only two tables. Every other list table still grew without bound — the MCP-changes table ("Änderungen durch MCP-Aktionen, max. 50") most of all | shipped | Tim (2026-08-06): "make sure all those tables have a max-height and are scrollable when they are too big". `Ui::TABLE_SCROLL` + `Ui::TH_STICKY` now on all 9 list tables. Excluded: the Start status table (fixed key/value, no thead) and the markdown tables the Dokumentation tab renders from README.md. M |
| U45 | 2026-08-06 | Start — both | Nothing on the landing page said an AI provider is optional except one line inside a five-item checklist. A merchant reading the plugin as "needs a paid AI subscription" simply stops | shipped | Tim (2026-08-06): "make sure to explain on Start tab that a AI (KI) service is not necessary… the api key is only needed if the customer wants AI from within the shop". New card "Brauche ich dafür einen KI-Dienst?" naming both directions, plus a sharper `ai` checklist row; both regression-locked in `SetupChecklistTest`. M |
