# Bar-Model Solutions — Pipeline, Rendering & Deploy Reference

Hard-won operational notes for the AI bar-model solutions (P3–P6). Pairs with
`bar-models.md` (the *authoring/visual* spec). Read this before touching the
solutions, the model images, or the admin/app rendering.

Tooling lives in `_work_barmodels/` (untracked working dir): `lib.php` (engine),
`pilot.php` + `lvl_p3..p6.php` + `lvl_fix14.php` + `lvl_batch1.php` (authored
solutions), `build_deploy*.php` (generate prod deploy scripts), `preview.php`,
plus prod fetch/verify/publish scripts. Run a file with
`php artisan tinker _work_barmodels/<file>.php`.

## 1. Delivery format — STRING + PNG (not HTML/SVG)

The app and admin renderers do NOT render arbitrary HTML/SVG in the solution
field. A solution is stored as a **text body**:

```
<intro> <br><br> [[model:questions/solutions/q<id>.png]] <br><br>
• <step> <br> • <step> ... <br><br> <answer> <br> Check: ... ✓
```

- The drawn model is a **PNG** rasterised from an SVG (headless Chrome), served
  from `/media/...`. The body references it with the `[[model:path]]` marker.
- Fractions/math use LaTeX **`$$\frac{a}{b}$$`** (see §3). Operators are unicode
  (`→ − × ÷`), line breaks are literal `<br>`.
- PNGs live at `storage/app/public/questions/solutions/q<id>.png`, served at
  `/media/questions/solutions/q<id>.png`.

## 2. Surfaces & their renderers (each renders differently!)

**CRITICAL — two separate admin systems:**
- **`/admin/...` = the LEGACY custom admin (controllers + Blade)** — this is what
  the team uses today. Question screens are `QuestionController` → Blade views.
- **`/cp/...` = the Filament panel** — the future migration target, NOT live yet.
  Editing `QuestionResource` (form/infolist) only affects `/cp`, which nobody is
  using — so a "the solution still doesn't show" on `/admin` is NOT fixed there.
  (Leave `/cp` to whoever owns the Filament migration.)

| Surface | URL | Renderer | where to fix `[[model]]` |
|---|---|---|---|
| Student app | quiz.allgifted.com (flutter_demo=flutterquiz repo) | `MathTextUtils.renderSolution`, LaTeX via flutter_math_fork | client resolves marker→Image.network |
| QA review (legacy) | `/admin/qa/questions/{id}/review` → `QAController@reviewQuestion` → `resources/views/admin/qa/show.blade.php` | Blade + KaTeX (`.solution-content,.hint-item`) | preg_replace `[[model]]`→`<img>` in the blade |
| Question edit/view (legacy) | `/admin/questions/{id}` (`?edit=1` = inline edit) → `QuestionController@show` → `resources/views/admin/questions/show.blade.php` | Blade + `math-render` KaTeX | preg_replace `[[model]]`→`<img>` at the solution (line ~399) AND hint (line ~349) `rich-content` blocks; leave the edit `<textarea>` raw |
| Filament (FUTURE) | `/cp/...` → `QuestionResource` form()/infolist() | not live | n/a yet — separate effort |

- Flutter renderers handle ONLY: LaTeX, `<br>/<hr>`, `[[model:png]]`, plain text.
  **No general HTML**. `renderMathText` does NOT handle `[[model]]` — only
  `renderSolution` does (hints were switched to it).
- Both legacy blades output the body raw (`{!! !!}`) with a `math-render`/KaTeX
  class, so fractions render but `[[model]]` stays literal unless you resolve it.
- The model `<img>` src must carry `?t=time()` (immutable `/media` cache, §5).

## 3. Fraction & encoding rules (most bugs lived here)

- **Body fractions:** `$$\frac{a}{b}$$`. `\(...\)` does NOT resolve on the
  KaTeX/app renderers — use `$$...$$`. NEVER vulgar-fraction glyphs / `&#85xx;` /
  `&fracNN;` (they exist for only some fractions and silently render the wrong
  value).
- **`frac()` must CONCATENATE**, never `"\frac{$a}{$b}"` — in a double-quoted PHP
  string `{$a}` is interpolation that **eats the braces** → `\frac243` (LaTeX
  reads as ²⁄₄·3). Correct: `'$$\\frac{' . $a . '}{' . $b . '}$$'`.
- **SVG `<text>` labels:** use `sfrac()` = plain `a/b`. SVG can't render LaTeX or
  HTML entities — `frac()`/`$$..$$`/`&times;`/`&minus;`/`\(..\)` show *literally*
  in the PNG. `_svgtext()` defends (strips `$$`, `\(\)`, `\frac{}{}`→`a/b`,
  decodes entities→unicode) — but prefer `sfrac()` at the source.
- Body operators: `_txt()` converts `&rarr;`/`&minus;`/`&times;`/`&divide;`→
  unicode (those render as plain text in the app/admin).

## 4. Other conventions

- **Hints = concise TEXT steps** (L1 "draw the bar model: …", L2..n one working
  step each). The bar model image goes in the **solution only** — do NOT embed an
  interim/transformed model image inside a hint. We tried it (the "equalised"
  picture) and reverted 2026-06-09: it's a near-duplicate of the main model and
  confuses more than it helps. (`renderSolution` *can* show `[[model]]` in a hint;
  we just don't use it.) Never let a hint before the last one state the answer.


- **Given vs derived:** given = plain; derived (worked-out) = italic + dotted
  underline. Pass `$derived=true` to `seg/gapseg/vbrace/hbrace/tbrace`.
- **Solid = quantity, dashed = gap:** a difference/gap between bars uses
  `gapseg()` (dashed, no fill), never a solid `seg()`.
- **Answer storage:** type 2 (FIB) answers in `answer0..answer3`; type 1 (MCQ)
  `correct_answer` is the **0-based index** of the correct option (NOT the value).
- **Visibility:** a solution shows only if `solutions.status_id=3` AND
  (`source='human'` OR `human_checked_at` set) AND the user is premium
  (`MathTutorController::solution`). AND the **question** must be public
  (`questions.status_id=3`) or the app never loads it.

## 5. Deploy & ops

- **Prod:** `ssh root@152.42.223.228` (IP + HostKeyAlias, never hostname),
  `/var/www/html/mathapi`. Network to it is flaky — retry scp/ssh.
- **Prod tracks `feat/b2b-tenancy` but is BEHIND it** (at an older commit) with a
  dirty tree. A `git pull` would deploy all the WIP — so single fixes are pushed
  by **targeted `scp` of one file** (creates drift vs git until a real b2b
  deploy reconciles). Data (solutions/hints/question status) is deployed by
  **direct idempotent DB writes via tinker**, never git.
- **`/media` is `Cache-Control: immutable` (1yr).** Overwriting a PNG with the
  same filename will NOT show (browser serves the cached old one). Either
  hard-refresh, or **cache-bust**: admin `<img>` tags append `?t=time()`; for the
  student app, version the filename. Symptom: "the pic still shows the old
  thing / nothing updated" — it's almost always this.
- **Filament caches the form/infolist schema** (`bootstrap/cache/filament/`).
  After editing a Filament Resource: `php artisan filament:cache-components` then
  `systemctl restart apache2` (full restart to flush opcache; graceful reload
  may not). Also: form-schema `Placeholder->content(Closure)` breaks under
  component caching — use a Blade `View` partial instead.
- After any prod artisan-as-root: `chown -R www-data:www-data storage bootstrap/cache`.
- **Hints** table has a UNIQUE key `(question_id, hint_level)`. Preserve any
  human hints (`user_id=41`); insert ours (`user_id=2`) with `insertOrIgnore`.
- PNG deploy = `scp` to `storage/app/public/questions/solutions/` then chown.

## 6. State (2026-06-09)

- **140 solutions live** on prod (status 3 + human_checked_at): the original 128
  + batch1 of 12. All questions published (status 3); models served via /media.
- Admin (QA review + Filament edit/view) render solutions, model images, and
  KaTeX fractions, with `?t=` cache-busting.
- **Student app renderer changes committed (`4367f72`, flutterquiz
  `rename/ags-math-brand`) but NOT built/deployed** — solutions show in admin,
  not yet in the live app.
- **~228 candidate questions remain** unauthored. Author against **prod** text
  (local DB is a stale snapshot; verify each answer == prod). Skip corrupted
  question text (e.g. #5269 `\dfrac{5600}{1}`) — flag for a question fix.
- Git: authoring tooling/guide on `feat/bar-model` (off master); admin display
  partials committed `ea1d6230` on `feat/b2b-tenancy`.

## 7. Bug log (do not repeat)

1. Inline HTML/SVG in the solution field → renders as raw text. Use string+PNG.
2. `\(...\)` fractions don't resolve → use `$$...$$`.
3. `"\frac{$a}{$b}"` ate the braces → `\frac243`. Concatenate.
4. `frac()`/`$$`/`&times;`/`&minus;` inside SVG labels → literal in the PNG. Use
   `sfrac()`/plain unicode (`_svgtext` defends).
5. Headless Chrome renders blank below ~110px window → min height 260 + GD trim.
6. Immutable `/media` cache → same-filename PNG overwrites invisible → `?t=` bust.
7. Filament component cache + `Placeholder` closure → marker unresolved. Use View.
8. Filament `ViewRecord` auto-infolist drops custom `View` → define `infolist()`.
9. Solution invisible because the QUESTION was private (status 1) — publish it.
10. MCQ "answer mismatches" were `correct_answer`-index misreads, not bugs.
11. **`/admin` ≠ `/cp`.** Spent a long time "fixing" the Filament `QuestionResource`
    (which serves `/cp`, not live) while the team was on the legacy `/admin`
    (`QuestionController` + `admin/questions/show.blade.php`). Always confirm WHICH
    admin the user is on before editing. `/admin` = legacy Blade; `/cp` = Filament.
12. **Don't fan out SSH-heavy agents at prod.** Launching 5 parallel agents + the
    main loop, each with SSH/scp retry loops, tripped the droplet's SSH guard
    (fail2ban / DO firewall) and got this machine's IP BLOCKED on port 22 (443
    stayed open). Do prod conversions SERIALLY — one primary at a time, few SSH
    connections per batch (fetch-all → deploy-all in one session, no tight retry
    storms). Recover a block via the DO web console: `fail2ban-client unban <ip>`
    / check the firewall SSH allowlist.
13. **Worktree isolation does NOT cover `_work_barmodels/`** — it's untracked, so
    every agent's worktree shares the one real `lib.php` (they collided on the
    Chrome `user-data-dir`). Parallel authoring needs per-agent COPIES of the
    tooling, or just run serially.
